diff --git a/app/schemas/com.nextcloud.client.database.NextcloudDatabase/96.json b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/96.json new file mode 100644 index 000000000000..4dc4849f5551 --- /dev/null +++ b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/96.json @@ -0,0 +1,1293 @@ +{ + "formatVersion": 1, + "database": { + "version": 96, + "identityHash": "0e9718354266517a340a89e16bb7d373", + "entities": [ + { + "tableName": "arbitrary_data", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `cloud_id` TEXT, `key` TEXT, `value` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "cloudId", + "columnName": "cloud_id", + "affinity": "TEXT" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "capabilities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `assistant` INTEGER, `account` TEXT, `version_mayor` INTEGER, `version_minor` INTEGER, `version_micro` INTEGER, `version_string` TEXT, `version_edition` TEXT, `extended_support` INTEGER, `core_pollinterval` INTEGER, `sharing_api_enabled` INTEGER, `sharing_public_enabled` INTEGER, `sharing_public_password_enforced` INTEGER, `sharing_public_expire_date_enabled` INTEGER, `sharing_public_expire_date_days` INTEGER, `sharing_public_expire_date_enforced` INTEGER, `sharing_public_send_mail` INTEGER, `sharing_public_upload` INTEGER, `sharing_user_send_mail` INTEGER, `sharing_resharing` INTEGER, `sharing_federation_outgoing` INTEGER, `sharing_federation_incoming` INTEGER, `files_bigfilechunking` INTEGER, `files_undelete` INTEGER, `files_versioning` INTEGER, `external_links` INTEGER, `server_name` TEXT, `server_color` TEXT, `server_text_color` TEXT, `server_element_color` TEXT, `server_slogan` TEXT, `server_logo` TEXT, `background_url` TEXT, `end_to_end_encryption` INTEGER, `end_to_end_encryption_keys_exist` INTEGER, `end_to_end_encryption_api_version` TEXT, `activity` INTEGER, `background_default` INTEGER, `background_plain` INTEGER, `richdocument` INTEGER, `richdocument_mimetype_list` TEXT, `richdocument_direct_editing` INTEGER, `richdocument_direct_templates` INTEGER, `richdocument_optional_mimetype_list` TEXT, `sharing_public_ask_for_optional_password` INTEGER, `richdocument_product_name` TEXT, `direct_editing_etag` TEXT, `user_status` INTEGER, `user_status_supports_emoji` INTEGER, `etag` TEXT, `files_locking_version` TEXT, `groupfolders` INTEGER, `drop_account` INTEGER, `security_guard` INTEGER, `forbidden_filename_characters` INTEGER, `forbidden_filenames` INTEGER, `forbidden_filename_extensions` INTEGER, `forbidden_filename_basenames` INTEGER, `files_download_limit` INTEGER, `files_download_limit_default` INTEGER, `recommendation` INTEGER, `notes_folder_path` TEXT, `default_permissions` INTEGER, `user_status_supports_busy` INTEGER, `windows_compatible_filenames` INTEGER, `has_valid_subscription` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "assistant", + "columnName": "assistant", + "affinity": "INTEGER" + }, + { + "fieldPath": "accountName", + "columnName": "account", + "affinity": "TEXT" + }, + { + "fieldPath": "versionMajor", + "columnName": "version_mayor", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionMinor", + "columnName": "version_minor", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionMicro", + "columnName": "version_micro", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionString", + "columnName": "version_string", + "affinity": "TEXT" + }, + { + "fieldPath": "versionEditor", + "columnName": "version_edition", + "affinity": "TEXT" + }, + { + "fieldPath": "extendedSupport", + "columnName": "extended_support", + "affinity": "INTEGER" + }, + { + "fieldPath": "corePollinterval", + "columnName": "core_pollinterval", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingApiEnabled", + "columnName": "sharing_api_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicEnabled", + "columnName": "sharing_public_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicPasswordEnforced", + "columnName": "sharing_public_password_enforced", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateEnabled", + "columnName": "sharing_public_expire_date_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateDays", + "columnName": "sharing_public_expire_date_days", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateEnforced", + "columnName": "sharing_public_expire_date_enforced", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicSendMail", + "columnName": "sharing_public_send_mail", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicUpload", + "columnName": "sharing_public_upload", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingUserSendMail", + "columnName": "sharing_user_send_mail", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingResharing", + "columnName": "sharing_resharing", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingFederationOutgoing", + "columnName": "sharing_federation_outgoing", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingFederationIncoming", + "columnName": "sharing_federation_incoming", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesBigfilechunking", + "columnName": "files_bigfilechunking", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesUndelete", + "columnName": "files_undelete", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesVersioning", + "columnName": "files_versioning", + "affinity": "INTEGER" + }, + { + "fieldPath": "externalLinks", + "columnName": "external_links", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT" + }, + { + "fieldPath": "serverColor", + "columnName": "server_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverTextColor", + "columnName": "server_text_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverElementColor", + "columnName": "server_element_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverSlogan", + "columnName": "server_slogan", + "affinity": "TEXT" + }, + { + "fieldPath": "serverLogo", + "columnName": "server_logo", + "affinity": "TEXT" + }, + { + "fieldPath": "serverBackgroundUrl", + "columnName": "background_url", + "affinity": "TEXT" + }, + { + "fieldPath": "endToEndEncryption", + "columnName": "end_to_end_encryption", + "affinity": "INTEGER" + }, + { + "fieldPath": "endToEndEncryptionKeysExist", + "columnName": "end_to_end_encryption_keys_exist", + "affinity": "INTEGER" + }, + { + "fieldPath": "endToEndEncryptionApiVersion", + "columnName": "end_to_end_encryption_api_version", + "affinity": "TEXT" + }, + { + "fieldPath": "activity", + "columnName": "activity", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverBackgroundDefault", + "columnName": "background_default", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverBackgroundPlain", + "columnName": "background_plain", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocument", + "columnName": "richdocument", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentMimetypeList", + "columnName": "richdocument_mimetype_list", + "affinity": "TEXT" + }, + { + "fieldPath": "richdocumentDirectEditing", + "columnName": "richdocument_direct_editing", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentTemplates", + "columnName": "richdocument_direct_templates", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentOptionalMimetypeList", + "columnName": "richdocument_optional_mimetype_list", + "affinity": "TEXT" + }, + { + "fieldPath": "sharingPublicAskForOptionalPassword", + "columnName": "sharing_public_ask_for_optional_password", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentProductName", + "columnName": "richdocument_product_name", + "affinity": "TEXT" + }, + { + "fieldPath": "directEditingEtag", + "columnName": "direct_editing_etag", + "affinity": "TEXT" + }, + { + "fieldPath": "userStatus", + "columnName": "user_status", + "affinity": "INTEGER" + }, + { + "fieldPath": "userStatusSupportsEmoji", + "columnName": "user_status_supports_emoji", + "affinity": "INTEGER" + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT" + }, + { + "fieldPath": "filesLockingVersion", + "columnName": "files_locking_version", + "affinity": "TEXT" + }, + { + "fieldPath": "groupfolders", + "columnName": "groupfolders", + "affinity": "INTEGER" + }, + { + "fieldPath": "dropAccount", + "columnName": "drop_account", + "affinity": "INTEGER" + }, + { + "fieldPath": "securityGuard", + "columnName": "security_guard", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFileNameCharacters", + "columnName": "forbidden_filename_characters", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFileNames", + "columnName": "forbidden_filenames", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFileNameExtensions", + "columnName": "forbidden_filename_extensions", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFilenameBaseNames", + "columnName": "forbidden_filename_basenames", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesDownloadLimit", + "columnName": "files_download_limit", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesDownloadLimitDefault", + "columnName": "files_download_limit_default", + "affinity": "INTEGER" + }, + { + "fieldPath": "recommendation", + "columnName": "recommendation", + "affinity": "INTEGER" + }, + { + "fieldPath": "notesFolderPath", + "columnName": "notes_folder_path", + "affinity": "TEXT" + }, + { + "fieldPath": "defaultPermissions", + "columnName": "default_permissions", + "affinity": "INTEGER" + }, + { + "fieldPath": "userStatusSupportsBusy", + "columnName": "user_status_supports_busy", + "affinity": "INTEGER" + }, + { + "fieldPath": "isWCFEnabled", + "columnName": "windows_compatible_filenames", + "affinity": "INTEGER" + }, + { + "fieldPath": "hasValidSubscription", + "columnName": "has_valid_subscription", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "external_links", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `icon_url` TEXT, `language` TEXT, `type` INTEGER, `name` TEXT, `url` TEXT, `redirect` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT" + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT" + }, + { + "fieldPath": "redirect", + "columnName": "redirect", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "filelist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `filename` TEXT, `encrypted_filename` TEXT, `path` TEXT, `path_decrypted` TEXT, `parent` INTEGER, `created` INTEGER, `modified` INTEGER, `content_type` TEXT, `content_length` INTEGER, `media_path` TEXT, `file_owner` TEXT, `last_sync_date` INTEGER, `last_sync_date_for_data` INTEGER, `modified_at_last_sync_for_data` INTEGER, `etag` TEXT, `etag_on_server` TEXT, `share_by_link` INTEGER, `permissions` TEXT, `remote_id` TEXT, `local_id` INTEGER NOT NULL DEFAULT -1, `update_thumbnail` INTEGER, `is_downloading` INTEGER, `favorite` INTEGER, `hidden` INTEGER, `is_encrypted` INTEGER, `etag_in_conflict` TEXT, `shared_via_users` INTEGER, `mount_type` INTEGER, `has_preview` INTEGER, `unread_comments_count` INTEGER, `owner_id` TEXT, `owner_display_name` TEXT, `note` TEXT, `sharees` TEXT, `rich_workspace` TEXT, `metadata_size` TEXT, `metadata_live_photo` TEXT, `locked` INTEGER, `lock_type` INTEGER, `lock_owner` TEXT, `lock_owner_display_name` TEXT, `lock_owner_editor` TEXT, `lock_timestamp` INTEGER, `lock_timeout` INTEGER, `lock_token` TEXT, `tags` TEXT, `metadata_gps` TEXT, `e2e_counter` INTEGER, `internal_two_way_sync_timestamp` INTEGER, `internal_two_way_sync_result` TEXT, `uploaded` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "filename", + "affinity": "TEXT" + }, + { + "fieldPath": "encryptedName", + "columnName": "encrypted_filename", + "affinity": "TEXT" + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT" + }, + { + "fieldPath": "pathDecrypted", + "columnName": "path_decrypted", + "affinity": "TEXT" + }, + { + "fieldPath": "parent", + "columnName": "parent", + "affinity": "INTEGER" + }, + { + "fieldPath": "creation", + "columnName": "created", + "affinity": "INTEGER" + }, + { + "fieldPath": "modified", + "columnName": "modified", + "affinity": "INTEGER" + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT" + }, + { + "fieldPath": "contentLength", + "columnName": "content_length", + "affinity": "INTEGER" + }, + { + "fieldPath": "storagePath", + "columnName": "media_path", + "affinity": "TEXT" + }, + { + "fieldPath": "accountOwner", + "columnName": "file_owner", + "affinity": "TEXT" + }, + { + "fieldPath": "lastSyncDate", + "columnName": "last_sync_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastSyncDateForData", + "columnName": "last_sync_date_for_data", + "affinity": "INTEGER" + }, + { + "fieldPath": "modifiedAtLastSyncForData", + "columnName": "modified_at_last_sync_for_data", + "affinity": "INTEGER" + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT" + }, + { + "fieldPath": "etagOnServer", + "columnName": "etag_on_server", + "affinity": "TEXT" + }, + { + "fieldPath": "sharedViaLink", + "columnName": "share_by_link", + "affinity": "INTEGER" + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "TEXT" + }, + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT" + }, + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "updateThumbnail", + "columnName": "update_thumbnail", + "affinity": "INTEGER" + }, + { + "fieldPath": "isDownloading", + "columnName": "is_downloading", + "affinity": "INTEGER" + }, + { + "fieldPath": "favorite", + "columnName": "favorite", + "affinity": "INTEGER" + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "isEncrypted", + "columnName": "is_encrypted", + "affinity": "INTEGER" + }, + { + "fieldPath": "etagInConflict", + "columnName": "etag_in_conflict", + "affinity": "TEXT" + }, + { + "fieldPath": "sharedWithSharee", + "columnName": "shared_via_users", + "affinity": "INTEGER" + }, + { + "fieldPath": "mountType", + "columnName": "mount_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "hasPreview", + "columnName": "has_preview", + "affinity": "INTEGER" + }, + { + "fieldPath": "unreadCommentsCount", + "columnName": "unread_comments_count", + "affinity": "INTEGER" + }, + { + "fieldPath": "ownerId", + "columnName": "owner_id", + "affinity": "TEXT" + }, + { + "fieldPath": "ownerDisplayName", + "columnName": "owner_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT" + }, + { + "fieldPath": "sharees", + "columnName": "sharees", + "affinity": "TEXT" + }, + { + "fieldPath": "richWorkspace", + "columnName": "rich_workspace", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataSize", + "columnName": "metadata_size", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataLivePhoto", + "columnName": "metadata_live_photo", + "affinity": "TEXT" + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockType", + "columnName": "lock_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockOwner", + "columnName": "lock_owner", + "affinity": "TEXT" + }, + { + "fieldPath": "lockOwnerDisplayName", + "columnName": "lock_owner_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "lockOwnerEditor", + "columnName": "lock_owner_editor", + "affinity": "TEXT" + }, + { + "fieldPath": "lockTimestamp", + "columnName": "lock_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockTimeout", + "columnName": "lock_timeout", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockToken", + "columnName": "lock_token", + "affinity": "TEXT" + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataGPS", + "columnName": "metadata_gps", + "affinity": "TEXT" + }, + { + "fieldPath": "e2eCounter", + "columnName": "e2e_counter", + "affinity": "INTEGER" + }, + { + "fieldPath": "internalTwoWaySync", + "columnName": "internal_two_way_sync_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "internalTwoWaySyncResult", + "columnName": "internal_two_way_sync_result", + "affinity": "TEXT" + }, + { + "fieldPath": "uploaded", + "columnName": "uploaded", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "filesystem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `is_folder` INTEGER, `found_at` INTEGER, `upload_triggered` INTEGER, `syncedfolder_id` TEXT, `crc32` TEXT, `modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "fileIsFolder", + "columnName": "is_folder", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileFoundRecently", + "columnName": "found_at", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileSentForUpload", + "columnName": "upload_triggered", + "affinity": "INTEGER" + }, + { + "fieldPath": "syncedFolderId", + "columnName": "syncedfolder_id", + "affinity": "TEXT" + }, + { + "fieldPath": "crc32", + "columnName": "crc32", + "affinity": "TEXT" + }, + { + "fieldPath": "fileModified", + "columnName": "modified_at", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "ocshares", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `file_source` INTEGER, `item_source` INTEGER, `share_type` INTEGER, `shate_with` TEXT, `path` TEXT, `permissions` INTEGER, `shared_date` INTEGER, `expiration_date` INTEGER, `token` TEXT, `shared_with_display_name` TEXT, `is_directory` INTEGER, `user_id` TEXT, `id_remote_shared` INTEGER, `owner_share` TEXT, `is_password_protected` INTEGER, `note` TEXT, `hide_download` INTEGER, `share_link` TEXT, `share_label` TEXT, `download_limit_limit` INTEGER, `download_limit_count` INTEGER, `attributes` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileSource", + "columnName": "file_source", + "affinity": "INTEGER" + }, + { + "fieldPath": "itemSource", + "columnName": "item_source", + "affinity": "INTEGER" + }, + { + "fieldPath": "shareType", + "columnName": "share_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "shareWith", + "columnName": "shate_with", + "affinity": "TEXT" + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT" + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharedDate", + "columnName": "shared_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "expirationDate", + "columnName": "expiration_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT" + }, + { + "fieldPath": "shareWithDisplayName", + "columnName": "shared_with_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "isDirectory", + "columnName": "is_directory", + "affinity": "INTEGER" + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT" + }, + { + "fieldPath": "idRemoteShared", + "columnName": "id_remote_shared", + "affinity": "INTEGER" + }, + { + "fieldPath": "accountOwner", + "columnName": "owner_share", + "affinity": "TEXT" + }, + { + "fieldPath": "isPasswordProtected", + "columnName": "is_password_protected", + "affinity": "INTEGER" + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT" + }, + { + "fieldPath": "hideDownload", + "columnName": "hide_download", + "affinity": "INTEGER" + }, + { + "fieldPath": "shareLink", + "columnName": "share_link", + "affinity": "TEXT" + }, + { + "fieldPath": "shareLabel", + "columnName": "share_label", + "affinity": "TEXT" + }, + { + "fieldPath": "downloadLimitLimit", + "columnName": "download_limit_limit", + "affinity": "INTEGER" + }, + { + "fieldPath": "downloadLimitCount", + "columnName": "download_limit_count", + "affinity": "INTEGER" + }, + { + "fieldPath": "attributes", + "columnName": "attributes", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "synced_folders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `wifi_only` INTEGER, `charging_only` INTEGER, `existing` INTEGER, `enabled` INTEGER, `enabled_timestamp_ms` INTEGER, `subfolder_by_date` INTEGER, `account` TEXT, `upload_option` INTEGER, `name_collision_policy` INTEGER, `type` INTEGER, `hidden` INTEGER, `sub_folder_rule` INTEGER, `exclude_hidden` INTEGER, `last_scan_timestamp_ms` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT" + }, + { + "fieldPath": "wifiOnly", + "columnName": "wifi_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "chargingOnly", + "columnName": "charging_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "existing", + "columnName": "existing", + "affinity": "INTEGER" + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "enabledTimestampMs", + "columnName": "enabled_timestamp_ms", + "affinity": "INTEGER" + }, + { + "fieldPath": "subfolderByDate", + "columnName": "subfolder_by_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT" + }, + { + "fieldPath": "uploadAction", + "columnName": "upload_option", + "affinity": "INTEGER" + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER" + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "subFolderRule", + "columnName": "sub_folder_rule", + "affinity": "INTEGER" + }, + { + "fieldPath": "excludeHidden", + "columnName": "exclude_hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastScanTimestampMs", + "columnName": "last_scan_timestamp_ms", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "list_of_uploads", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `account_name` TEXT, `file_size` INTEGER, `status` INTEGER, `local_behaviour` INTEGER, `upload_time` INTEGER, `name_collision_policy` INTEGER, `is_create_remote_folder` INTEGER, `upload_end_timestamp` INTEGER, `last_result` INTEGER, `is_while_charging_only` INTEGER, `is_wifi_only` INTEGER, `created_by` INTEGER, `folder_unlock_token` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT" + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT" + }, + { + "fieldPath": "fileSize", + "columnName": "file_size", + "affinity": "INTEGER" + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER" + }, + { + "fieldPath": "localBehaviour", + "columnName": "local_behaviour", + "affinity": "INTEGER" + }, + { + "fieldPath": "uploadTime", + "columnName": "upload_time", + "affinity": "INTEGER" + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER" + }, + { + "fieldPath": "isCreateRemoteFolder", + "columnName": "is_create_remote_folder", + "affinity": "INTEGER" + }, + { + "fieldPath": "uploadEndTimestamp", + "columnName": "upload_end_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastResult", + "columnName": "last_result", + "affinity": "INTEGER" + }, + { + "fieldPath": "isWhileChargingOnly", + "columnName": "is_while_charging_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "isWifiOnly", + "columnName": "is_wifi_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "createdBy", + "columnName": "created_by", + "affinity": "INTEGER" + }, + { + "fieldPath": "folderUnlockToken", + "columnName": "folder_unlock_token", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "virtual", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `type` TEXT, `ocfile_id` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT" + }, + { + "fieldPath": "ocFileId", + "columnName": "ocfile_id", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "offline_operations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `offline_operations_parent_oc_file_id` INTEGER, `offline_operations_path` TEXT, `offline_operations_type` TEXT, `offline_operations_file_name` TEXT, `offline_operations_created_at` INTEGER, `offline_operations_modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "parentOCFileId", + "columnName": "offline_operations_parent_oc_file_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "path", + "columnName": "offline_operations_path", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "offline_operations_type", + "affinity": "TEXT" + }, + { + "fieldPath": "filename", + "columnName": "offline_operations_file_name", + "affinity": "TEXT" + }, + { + "fieldPath": "createdAt", + "columnName": "offline_operations_created_at", + "affinity": "INTEGER" + }, + { + "fieldPath": "modifiedAt", + "columnName": "offline_operations_modified_at", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "recommended_files", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `directory` TEXT NOT NULL, `extension` TEXT NOT NULL, `mime_type` TEXT NOT NULL, `has_preview` INTEGER NOT NULL, `reason` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `account_name` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "directory", + "columnName": "directory", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "extension", + "columnName": "extension", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mimeType", + "columnName": "mime_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasPreview", + "columnName": "has_preview", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reason", + "columnName": "reason", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "assistant", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountName` TEXT, `type` TEXT, `status` TEXT, `userId` TEXT, `appId` TEXT, `input` TEXT, `output` TEXT, `completionExpectedAt` INTEGER, `progress` INTEGER, `lastUpdated` INTEGER, `scheduledAt` INTEGER, `endedAt` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "accountName", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT" + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT" + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT" + }, + { + "fieldPath": "appId", + "columnName": "appId", + "affinity": "TEXT" + }, + { + "fieldPath": "input", + "columnName": "input", + "affinity": "TEXT" + }, + { + "fieldPath": "output", + "columnName": "output", + "affinity": "TEXT" + }, + { + "fieldPath": "completionExpectedAt", + "columnName": "completionExpectedAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER" + }, + { + "fieldPath": "scheduledAt", + "columnName": "scheduledAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "endedAt", + "columnName": "endedAt", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0e9718354266517a340a89e16bb7d373')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/nextcloud/client/assistant/AssistantRepositoryTests.kt b/app/src/androidTest/java/com/nextcloud/client/assistant/AssistantRepositoryTests.kt index e7cb55f0d67d..5463af89bf34 100644 --- a/app/src/androidTest/java/com/nextcloud/client/assistant/AssistantRepositoryTests.kt +++ b/app/src/androidTest/java/com/nextcloud/client/assistant/AssistantRepositoryTests.kt @@ -7,7 +7,7 @@ */ package com.nextcloud.client.assistant -import com.nextcloud.client.assistant.repository.AssistantRepository +import com.nextcloud.client.assistant.repository.remote.AssistantRemoteRepositoryImpl import com.owncloud.android.AbstractOnServerIT import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData import com.owncloud.android.lib.resources.status.NextcloudVersion @@ -18,11 +18,11 @@ import org.junit.Test @Suppress("MagicNumber") class AssistantRepositoryTests : AbstractOnServerIT() { - private var sut: AssistantRepository? = null + private var sut: AssistantRemoteRepositoryImpl? = null @Before fun setup() { - sut = AssistantRepository(nextcloudClient, capability) + sut = AssistantRemoteRepositoryImpl(nextcloudClient, capability) } @Test diff --git a/app/src/main/java/com/nextcloud/client/assistant/AsssistantScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt similarity index 53% rename from app/src/main/java/com/nextcloud/client/assistant/AsssistantScreen.kt rename to app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt index e19c6dede0f6..a1931bf5a7a0 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AsssistantScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt @@ -8,40 +8,55 @@ package com.nextcloud.client.assistant import android.app.Activity -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FabPosition import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.pulltorefresh.PullToRefreshState +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text import androidx.compose.material3.pulltorefresh.pullToRefresh import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.nextcloud.client.assistant.component.AddTaskAlertDialog -import com.nextcloud.client.assistant.component.CenterText import com.nextcloud.client.assistant.extensions.getInputTitle import com.nextcloud.client.assistant.model.ScreenOverlayState import com.nextcloud.client.assistant.model.ScreenState -import com.nextcloud.client.assistant.repository.AssistantMockRepository +import com.nextcloud.client.assistant.repository.local.MockAssistantLocalRepository +import com.nextcloud.client.assistant.repository.remote.MockAssistantRemoteRepository import com.nextcloud.client.assistant.task.TaskView import com.nextcloud.client.assistant.taskTypes.TaskTypesRow import com.nextcloud.ui.composeActivity.ComposeActivity @@ -51,10 +66,11 @@ import com.owncloud.android.R import com.owncloud.android.lib.resources.assistant.v2.model.Task import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData import com.owncloud.android.lib.resources.status.OCCapability -import com.owncloud.android.utils.DisplayUtils import kotlinx.coroutines.delay import kotlinx.coroutines.launch +private const val PULL_TO_REFRESH_DELAY = 1500L + @Suppress("LongMethod") @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -68,94 +84,98 @@ fun AssistantScreen(viewModel: AssistantViewModel, capability: OCCapability, act val taskTypes by viewModel.taskTypes.collectAsState() val scope = rememberCoroutineScope() val pullRefreshState = rememberPullToRefreshState() + val snackbarHostState = remember { SnackbarHostState() } + + LaunchedEffect(messageId) { + messageId?.let { + snackbarHostState.showSnackbar(activity.getString(it)) + viewModel.updateSnackbarMessage(null) + } + } + + LaunchedEffect(Unit) { + viewModel.startTaskListPolling() + } + + DisposableEffect(Unit) { + onDispose { + viewModel.stopTaskListPolling() + } + } - @Suppress("MagicNumber") - Box( + Scaffold( modifier = Modifier.pullToRefresh( - screenState == ScreenState.Refreshing, + false, pullRefreshState, onRefresh = { scope.launch { - delay(1500) + delay(PULL_TO_REFRESH_DELAY) viewModel.fetchTaskList() } } - ) - ) { - ShowScreenState(screenState, selectedTaskType, taskTypes, viewModel, filteredTaskList, capability) - - ShowLinearProgressIndicator(screenState, pullRefreshState) - - AddFloatingActionButton( - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(16.dp), - selectedTaskType, - viewModel - ) - } - - showSnackBarMessage(messageId, activity, viewModel) - ShowOverlayState(screenOverlayState, activity, viewModel) -} - -@Composable -private fun ShowScreenState( - screenState: ScreenState?, - selectedTaskType: TaskTypeData?, - taskTypes: List?, - viewModel: AssistantViewModel, - filteredTaskList: List?, - capability: OCCapability -) { - when (screenState) { - ScreenState.Refreshing -> { - CenterText(text = stringResource(id = R.string.assistant_screen_loading)) + ), + topBar = { + taskTypes?.let { + TaskTypesRow(selectedTaskType, data = it) { task -> + viewModel.selectTaskType(task) + } + } + }, + floatingActionButton = { + if (!taskTypes.isNullOrEmpty()) { + AddTaskButton( + selectedTaskType, + viewModel + ) + } + }, + floatingActionButtonPosition = FabPosition.EndOverlay, + snackbarHost = { + SnackbarHost(snackbarHostState) } + ) { paddingValues -> + when (screenState) { + is ScreenState.EmptyContent -> { + val state = (screenState as ScreenState.EmptyContent) + EmptyContent( + paddingValues, + state.iconId, + state.descriptionId + ) + } - ScreenState.EmptyContent -> { - EmptyTaskList(selectedTaskType, taskTypes, viewModel) - } + ScreenState.Content -> { + AssistantContent( + paddingValues, + filteredTaskList ?: listOf(), + viewModel, + capability + ) + } - ScreenState.Content -> { - AssistantContent( - filteredTaskList ?: listOf(), - taskTypes, - selectedTaskType, - viewModel, - capability + else -> EmptyContent( + paddingValues, + R.drawable.spinner_inner, + R.string.assistant_screen_loading ) } - null -> Unit - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun ShowLinearProgressIndicator(screenState: ScreenState?, pullToRefreshState: PullToRefreshState) { - if (screenState == ScreenState.Refreshing) { - LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) - } else { LinearProgressIndicator( - progress = { pullToRefreshState.distanceFraction }, + progress = { pullRefreshState.distanceFraction }, modifier = Modifier.fillMaxWidth() ) + + OverlayState(screenOverlayState, activity, viewModel) } } @Composable -private fun AddFloatingActionButton( - modifier: Modifier, - selectedTaskType: TaskTypeData?, - viewModel: AssistantViewModel -) { +private fun AddTaskButton(selectedTaskType: TaskTypeData?, viewModel: AssistantViewModel) { FloatingActionButton( - modifier = modifier, onClick = { selectedTaskType?.let { val newState = ScreenOverlayState.AddTask(it, "") - viewModel.updateScreenState(newState) + viewModel.updateTaskListScreenState(newState) } } ) { @@ -163,20 +183,9 @@ private fun AddFloatingActionButton( } } -private fun showSnackBarMessage(messageId: Int?, activity: Activity, viewModel: AssistantViewModel) { - messageId?.let { - DisplayUtils.showSnackMessage( - activity, - activity.getString(it) - ) - - viewModel.updateSnackbarMessage(null) - } -} - @Suppress("LongMethod") @Composable -private fun ShowOverlayState(state: ScreenOverlayState?, activity: Activity, viewModel: AssistantViewModel) { +private fun OverlayState(state: ScreenOverlayState?, activity: Activity, viewModel: AssistantViewModel) { when (state) { is ScreenOverlayState.AddTask -> { AddTaskAlertDialog( @@ -189,7 +198,7 @@ private fun ShowOverlayState(state: ScreenOverlayState?, activity: Activity, vie } }, dismiss = { - viewModel.updateScreenState(null) + viewModel.updateTaskListScreenState(null) } ) } @@ -198,22 +207,22 @@ private fun ShowOverlayState(state: ScreenOverlayState?, activity: Activity, vie SimpleAlertDialog( title = stringResource(id = R.string.assistant_screen_delete_task_alert_dialog_title), description = stringResource(id = R.string.assistant_screen_delete_task_alert_dialog_description), - dismiss = { viewModel.updateScreenState(null) }, + dismiss = { viewModel.updateTaskListScreenState(null) }, onComplete = { viewModel.deleteTask(state.id) } ) } is ScreenOverlayState.TaskActions -> { val actions = state.getActions(activity, onEditCompleted = { addTask -> - viewModel.updateScreenState(addTask) + viewModel.updateTaskListScreenState(addTask) }, onDeleteCompleted = { deleteTask -> - viewModel.updateScreenState(deleteTask) + viewModel.updateTaskListScreenState(deleteTask) }) MoreActionsBottomSheet( title = state.task.getInputTitle(), actions = actions, - dismiss = { viewModel.updateScreenState(null) } + dismiss = { viewModel.updateTaskListScreenState(null) } ) } @@ -223,62 +232,59 @@ private fun ShowOverlayState(state: ScreenOverlayState?, activity: Activity, vie @Composable private fun AssistantContent( + paddingValues: PaddingValues, taskList: List, - taskTypes: List?, - selectedTaskType: TaskTypeData?, viewModel: AssistantViewModel, capability: OCCapability ) { - Column(modifier = Modifier.fillMaxSize()) { - taskTypes?.let { - TaskTypesRow(selectedTaskType, data = taskTypes) { task -> - viewModel.selectTaskType(task) - } - } - - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(12.dp) - ) { - items(taskList) { task -> - TaskView( - task, - capability, - showTaskActions = { - val newState = ScreenOverlayState.TaskActions(task) - viewModel.updateScreenState(newState) - } - ) - Spacer(modifier = Modifier.height(8.dp)) - } + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(12.dp), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally + ) { + items(taskList, key = { it.id }) { task -> + TaskView( + task, + capability, + showTaskActions = { + val newState = ScreenOverlayState.TaskActions(task) + viewModel.updateTaskListScreenState(newState) + } + ) + Spacer(modifier = Modifier.height(8.dp)) } } } @Composable -private fun EmptyTaskList( - selectedTaskType: TaskTypeData?, - taskTypes: List?, - viewModel: AssistantViewModel -) { +private fun EmptyContent(paddingValues: PaddingValues, iconId: Int?, descriptionId: Int) { Column( modifier = Modifier .fillMaxSize() - .padding(16.dp) + .padding(paddingValues) + .padding(16.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally ) { - taskTypes?.let { - TaskTypesRow(selectedTaskType, data = taskTypes) { task -> - viewModel.selectTaskType(task) - } + iconId?.let { + Image( + painter = painterResource(id = iconId), + modifier = Modifier.size(32.dp), + colorFilter = ColorFilter.tint(color = colorResource(R.color.text_color)), + contentDescription = "empty content icon" + ) Spacer(modifier = Modifier.height(8.dp)) } - CenterText( - text = stringResource( - id = R.string.assistant_screen_create_a_new_task_from_bottom_right_text - ) + Text( + text = stringResource(descriptionId), + fontSize = 18.sp, + textAlign = TextAlign.Center, + color = colorResource(R.color.text_color) ) } } @@ -287,11 +293,10 @@ private fun EmptyTaskList( @Composable @Preview private fun AssistantScreenPreview() { - val mockRepository = AssistantMockRepository() MaterialTheme( content = { AssistantScreen( - viewModel = AssistantViewModel(repository = mockRepository), + viewModel = getMockViewModel(false), activity = ComposeActivity(), capability = OCCapability().apply { versionMayor = 30 @@ -305,11 +310,10 @@ private fun AssistantScreenPreview() { @Composable @Preview private fun AssistantEmptyScreenPreview() { - val mockRepository = AssistantMockRepository(giveEmptyTasks = true) MaterialTheme( content = { AssistantScreen( - viewModel = AssistantViewModel(repository = mockRepository), + viewModel = getMockViewModel(true), activity = ComposeActivity(), capability = OCCapability().apply { versionMayor = 30 @@ -318,3 +322,13 @@ private fun AssistantEmptyScreenPreview() { } ) } + +private fun getMockViewModel(giveEmptyTasks: Boolean): AssistantViewModel { + val mockLocalRepository = MockAssistantLocalRepository() + val mockRemoteRepository = MockAssistantRemoteRepository(giveEmptyTasks) + return AssistantViewModel( + accountName = "test:localhost", + remoteRepository = mockRemoteRepository, + localRepository = mockLocalRepository + ) +} diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt index 6d123e2f6de7..6f64bb69b4f4 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt @@ -11,18 +11,31 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.nextcloud.client.assistant.model.ScreenOverlayState import com.nextcloud.client.assistant.model.ScreenState -import com.nextcloud.client.assistant.repository.AssistantRepositoryType +import com.nextcloud.client.assistant.repository.local.AssistantLocalRepository +import com.nextcloud.client.assistant.repository.remote.AssistantRemoteRepository import com.owncloud.android.R +import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.assistant.v2.model.Task import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -class AssistantViewModel(private val repository: AssistantRepositoryType) : ViewModel() { +class AssistantViewModel( + private val accountName: String, + private val remoteRepository: AssistantRemoteRepository, + private val localRepository: AssistantLocalRepository +) : ViewModel() { + + companion object { + private const val TAG = "AssistantViewModel" + private const val TASK_LIST_POLLING_INTERVAL_MS = 15_000L + } private val _screenState = MutableStateFlow(null) val screenState: StateFlow = _screenState @@ -44,14 +57,54 @@ class AssistantViewModel(private val repository: AssistantRepositoryType) : View private val _filteredTaskList = MutableStateFlow?>(null) val filteredTaskList: StateFlow?> = _filteredTaskList + private var taskPollingJob: Job? = null + init { fetchTaskTypes() } + // region task polling + fun startTaskListPolling() { + stopTaskListPolling() + + taskPollingJob = viewModelScope.launch(Dispatchers.IO) { + try { + while (isActive) { + Log_OC.d(TAG, "Polling task list...") + fetchTaskListSuspending() + delay(TASK_LIST_POLLING_INTERVAL_MS) + } + } finally { + Log_OC.d(TAG, "Polling coroutine cancelled") + } + } + } + + fun stopTaskListPolling() { + taskPollingJob?.cancel() + taskPollingJob = null + } + // endregion + + private suspend fun fetchTaskListSuspending() { + val cachedTasks = localRepository.getCachedTasks(accountName) + if (cachedTasks.isNotEmpty()) { + _filteredTaskList.value = cachedTasks.sortedByDescending { it.id } + } + + val taskType = _selectedTaskType.value?.id ?: return + val result = remoteRepository.getTaskList(taskType) + if (result != null) { + taskList = result + _filteredTaskList.value = taskList?.sortedByDescending { it.id } + localRepository.cacheTasks(result, accountName) + } + } + @Suppress("MagicNumber") fun createTask(input: String, taskType: TaskTypeData) { viewModelScope.launch(Dispatchers.IO) { - val result = repository.createTask(input, taskType) + val result = remoteRepository.createTask(input, taskType) val messageId = if (result.isSuccess) { R.string.assistant_screen_task_create_success_message @@ -76,15 +129,11 @@ class AssistantViewModel(private val repository: AssistantRepositoryType) : View private fun fetchTaskTypes() { viewModelScope.launch(Dispatchers.IO) { - val taskTypesResult = repository.getTaskTypes() - - if (taskTypesResult == null) { - updateSnackbarMessage(R.string.assistant_screen_task_types_error_state_message) - return@launch - } - - if (taskTypesResult.isEmpty()) { - updateSnackbarMessage(R.string.assistant_screen_task_list_empty_message) + val taskTypesResult = remoteRepository.getTaskTypes() + if (taskTypesResult == null || taskTypesResult.isEmpty()) { + _screenState.update { + ScreenState.emptyTaskTypes() + } return@launch } @@ -98,12 +147,17 @@ class AssistantViewModel(private val repository: AssistantRepositoryType) : View fun fetchTaskList() { viewModelScope.launch(Dispatchers.IO) { - _screenState.update { - ScreenState.Refreshing + // Try cached data first + val cachedTasks = localRepository.getCachedTasks(accountName) + if (cachedTasks.isNotEmpty()) { + _filteredTaskList.update { + cachedTasks.sortedByDescending { it.id } + } + updateTaskListScreenState() } val taskType = _selectedTaskType.value?.id ?: return@launch - val result = repository.getTaskList(taskType) + val result = remoteRepository.getTaskList(taskType) if (result != null) { taskList = result _filteredTaskList.update { @@ -111,19 +165,21 @@ class AssistantViewModel(private val repository: AssistantRepositoryType) : View task.id } } + + localRepository.cacheTasks(result, accountName) updateSnackbarMessage(null) } else { updateSnackbarMessage(R.string.assistant_screen_task_list_error_state_message) } - updateScreenState() + updateTaskListScreenState() } } - private fun updateScreenState() { + private fun updateTaskListScreenState() { _screenState.update { if (_filteredTaskList.value?.isEmpty() == true) { - ScreenState.EmptyContent + ScreenState.emptyTaskList() } else { ScreenState.Content } @@ -132,7 +188,7 @@ class AssistantViewModel(private val repository: AssistantRepositoryType) : View fun deleteTask(id: Long) { viewModelScope.launch(Dispatchers.IO) { - val result = repository.deleteTask(id) + val result = remoteRepository.deleteTask(id) val messageId = if (result.isSuccess) { R.string.assistant_screen_task_delete_success_message @@ -144,6 +200,7 @@ class AssistantViewModel(private val repository: AssistantRepositoryType) : View if (result.isSuccess) { removeTaskFromList(id) + localRepository.deleteTask(id, accountName) } } } @@ -154,7 +211,7 @@ class AssistantViewModel(private val repository: AssistantRepositoryType) : View } } - fun updateScreenState(value: ScreenOverlayState?) { + fun updateTaskListScreenState(value: ScreenOverlayState?) { _screenOverlayState.update { value } diff --git a/app/src/main/java/com/nextcloud/client/assistant/component/CenterText.kt b/app/src/main/java/com/nextcloud/client/assistant/component/CenterText.kt deleted file mode 100644 index cf3cefb6171e..000000000000 --- a/app/src/main/java/com/nextcloud/client/assistant/component/CenterText.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2024 Alper Ozturk - * SPDX-FileCopyrightText: 2024 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.nextcloud.client.assistant.component - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.sp -import com.owncloud.android.R - -@Composable -fun CenterText(text: String) { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text( - text = text, - fontSize = 18.sp, - textAlign = TextAlign.Center, - color = colorResource(R.color.text_color) - ) - } -} diff --git a/app/src/main/java/com/nextcloud/client/assistant/model/ScreenState.kt b/app/src/main/java/com/nextcloud/client/assistant/model/ScreenState.kt index 33e206c9d00e..3c2ba4d83e82 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/model/ScreenState.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/model/ScreenState.kt @@ -7,8 +7,24 @@ package com.nextcloud.client.assistant.model -enum class ScreenState { - Refreshing, - EmptyContent, - Content +import com.owncloud.android.R + +sealed class ScreenState { + data object Loading : ScreenState() + + data object Content : ScreenState() + + data class EmptyContent(val iconId: Int?, val descriptionId: Int) : ScreenState() + + companion object { + fun emptyTaskTypes(): ScreenState = EmptyContent( + descriptionId = R.string.assistant_screen_task_list_empty_warning, + iconId = null + ) + + fun emptyTaskList(): ScreenState = EmptyContent( + descriptionId = R.string.assistant_screen_create_a_new_task_from_bottom_right_text, + iconId = null + ) + } } diff --git a/app/src/main/java/com/nextcloud/client/assistant/repository/local/AssistantLocalRepository.kt b/app/src/main/java/com/nextcloud/client/assistant/repository/local/AssistantLocalRepository.kt new file mode 100644 index 000000000000..070c0c74a9c1 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/assistant/repository/local/AssistantLocalRepository.kt @@ -0,0 +1,17 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.assistant.repository.local + +import com.owncloud.android.lib.resources.assistant.v2.model.Task + +interface AssistantLocalRepository { + suspend fun cacheTasks(tasks: List, accountName: String) + suspend fun getCachedTasks(accountName: String): List + suspend fun insertTask(task: Task, accountName: String) + suspend fun deleteTask(id: Long, accountName: String) +} diff --git a/app/src/main/java/com/nextcloud/client/assistant/repository/local/AssistantLocalRepositoryImpl.kt b/app/src/main/java/com/nextcloud/client/assistant/repository/local/AssistantLocalRepositoryImpl.kt new file mode 100644 index 000000000000..ef6ba9365606 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/assistant/repository/local/AssistantLocalRepositoryImpl.kt @@ -0,0 +1,69 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.assistant.repository.local + +import com.nextcloud.client.database.dao.AssistantDao +import com.nextcloud.client.database.entity.AssistantEntity +import com.owncloud.android.lib.resources.assistant.v2.model.Task +import com.owncloud.android.lib.resources.assistant.v2.model.TaskInput +import com.owncloud.android.lib.resources.assistant.v2.model.TaskOutput + +class AssistantLocalRepositoryImpl(private val assistantDao: AssistantDao) : AssistantLocalRepository { + + override suspend fun cacheTasks(tasks: List, accountName: String) { + val entities = tasks.map { it.toEntity(accountName) } + assistantDao.insertAssistantTasks(entities) + } + + override suspend fun getCachedTasks(accountName: String): List { + val entities = assistantDao.getAssistantTasksByAccount(accountName) + return entities.map { it.toTask() } + } + + override suspend fun insertTask(task: Task, accountName: String) { + assistantDao.insertAssistantTask(task.toEntity(accountName)) + } + + override suspend fun deleteTask(id: Long, accountName: String) { + val cached = assistantDao.getAssistantTasksByAccount(accountName).firstOrNull { it.id == id } ?: return + assistantDao.deleteAssistantTask(cached) + } + + // region Mapping helpers + private fun Task.toEntity(accountName: String): AssistantEntity = AssistantEntity( + id = this.id, + accountName = accountName, + type = this.type, + status = this.status, + userId = this.userId, + appId = this.appId, + input = this.input?.input, + output = this.output?.output, + completionExpectedAt = this.completionExpectedAt, + progress = this.progress, + lastUpdated = this.lastUpdated, + scheduledAt = this.scheduledAt, + endedAt = this.endedAt + ) + + private fun AssistantEntity.toTask(): Task = Task( + id = this.id, + type = this.type, + status = this.status, + userId = this.userId, + appId = this.appId, + input = TaskInput(input = this.input), + output = TaskOutput(output = this.output), + completionExpectedAt = this.completionExpectedAt, + progress = this.progress, + lastUpdated = this.lastUpdated, + scheduledAt = this.scheduledAt, + endedAt = this.endedAt + ) + // endregion +} diff --git a/app/src/main/java/com/nextcloud/client/assistant/repository/local/MockAssistantLocalRepository.kt b/app/src/main/java/com/nextcloud/client/assistant/repository/local/MockAssistantLocalRepository.kt new file mode 100644 index 000000000000..c09065a5b867 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/assistant/repository/local/MockAssistantLocalRepository.kt @@ -0,0 +1,35 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.assistant.repository.local + +import com.owncloud.android.lib.resources.assistant.v2.model.Task +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +class MockAssistantLocalRepository : AssistantLocalRepository { + + private val tasks = mutableListOf() + private val mutex = Mutex() + + override suspend fun cacheTasks(tasks: List, accountName: String) { + mutex.withLock { + this.tasks.clear() + this.tasks.addAll(tasks) + } + } + + override suspend fun getCachedTasks(accountName: String): List = mutex.withLock { tasks.toList() } + + override suspend fun insertTask(task: Task, accountName: String) { + mutex.withLock { tasks.add(task) } + } + + override suspend fun deleteTask(id: Long, accountName: String) { + mutex.withLock { tasks.removeAll { it.id == id } } + } +} diff --git a/app/src/main/java/com/nextcloud/client/assistant/repository/AssistantRepositoryType.kt b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepository.kt similarity index 51% rename from app/src/main/java/com/nextcloud/client/assistant/repository/AssistantRepositoryType.kt rename to app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepository.kt index 048eee96143d..c46245bfd450 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/repository/AssistantRepositoryType.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepository.kt @@ -1,21 +1,22 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2024 Alper Ozturk - * SPDX-FileCopyrightText: 2024 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.nextcloud.client.assistant.repository + +package com.nextcloud.client.assistant.repository.remote import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.resources.assistant.v2.model.Task import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData -interface AssistantRepositoryType { +interface AssistantRemoteRepository { fun getTaskTypes(): List? fun createTask(input: String, taskType: TaskTypeData): RemoteOperationResult - fun getTaskList(taskType: String): List? + fun getTaskList(taskType: String): List? fun deleteTask(id: Long): RemoteOperationResult } diff --git a/app/src/main/java/com/nextcloud/client/assistant/repository/AssistantRepository.kt b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepositoryImpl.kt similarity index 90% rename from app/src/main/java/com/nextcloud/client/assistant/repository/AssistantRepository.kt rename to app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepositoryImpl.kt index 13830cd8788f..07545a3591ec 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/repository/AssistantRepository.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepositoryImpl.kt @@ -1,11 +1,10 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2024 Alper Ozturk - * SPDX-FileCopyrightText: 2024 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.nextcloud.client.assistant.repository +package com.nextcloud.client.assistant.repository.remote import com.nextcloud.common.NextcloudClient import com.owncloud.android.lib.common.operations.RemoteOperationResult @@ -24,7 +23,8 @@ import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData import com.owncloud.android.lib.resources.status.NextcloudVersion import com.owncloud.android.lib.resources.status.OCCapability -class AssistantRepository(private val client: NextcloudClient, capability: OCCapability) : AssistantRepositoryType { +class AssistantRemoteRepositoryImpl(private val client: NextcloudClient, capability: OCCapability) : + AssistantRemoteRepository { private val supportsV2 = capability.version.isNewerOrEqual(NextcloudVersion.nextcloud_30) diff --git a/app/src/main/java/com/nextcloud/client/assistant/repository/AssistantMockRepository.kt b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/MockAssistantRemoteRepository.kt similarity index 87% rename from app/src/main/java/com/nextcloud/client/assistant/repository/AssistantMockRepository.kt rename to app/src/main/java/com/nextcloud/client/assistant/repository/remote/MockAssistantRemoteRepository.kt index b1309373e2ed..b7acd88b41fd 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/repository/AssistantMockRepository.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/MockAssistantRemoteRepository.kt @@ -1,11 +1,10 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2024 Alper Ozturk - * SPDX-FileCopyrightText: 2024 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.nextcloud.client.assistant.repository +package com.nextcloud.client.assistant.repository.remote import com.nextcloud.utils.extensions.getRandomString import com.owncloud.android.lib.common.operations.RemoteOperationResult @@ -16,7 +15,7 @@ import com.owncloud.android.lib.resources.assistant.v2.model.TaskOutput import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData @Suppress("MagicNumber") -class AssistantMockRepository(private val giveEmptyTasks: Boolean = false) : AssistantRepositoryType { +class MockAssistantRemoteRepository(private val giveEmptyTasks: Boolean = false) : AssistantRemoteRepository { override fun getTaskTypes(): List = listOf( TaskTypeData( id = "core:text2text", diff --git a/app/src/main/java/com/nextcloud/client/assistant/taskDetail/TaskDetailBottomSheet.kt b/app/src/main/java/com/nextcloud/client/assistant/taskDetail/TaskDetailBottomSheet.kt index 4438714e036c..a52b1141e3f5 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/taskDetail/TaskDetailBottomSheet.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/taskDetail/TaskDetailBottomSheet.kt @@ -8,7 +8,9 @@ package com.nextcloud.client.assistant.taskDetail import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -17,6 +19,8 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -28,9 +32,11 @@ import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview @@ -54,29 +60,54 @@ fun TaskDetailBottomSheet(task: Task, showTaskActions: () -> Unit, dismiss: () - onDismissRequest = { dismiss() }, sheetState = sheetState ) { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(16.dp) - ) { - stickyHeader { - Row( - modifier = Modifier.fillMaxWidth() - ) { - Spacer(modifier = Modifier.weight(1f)) + Box { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + stickyHeader { + Row( + modifier = Modifier.fillMaxWidth() + ) { + Spacer(modifier = Modifier.weight(1f)) - IconButton(onClick = showTaskActions) { - Icon( - imageVector = Icons.Filled.MoreVert, - contentDescription = "More button", - tint = colorResource(R.color.text_color) - ) + IconButton(onClick = showTaskActions) { + Icon( + imageVector = Icons.Filled.MoreVert, + contentDescription = "More button", + tint = colorResource(R.color.text_color) + ) + } } } + + item { + InputOutputCard(task) + } } - item { - InputOutputCard(task) + Row( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .padding(bottom = 16.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(R.drawable.ic_assistant), + contentDescription = "assistant icon", + modifier = Modifier.size(12.dp) + ) + + Spacer(modifier = Modifier.width(4.dp)) + + Text( + text = stringResource(R.string.assistant_generation_warning), + color = colorResource(R.color.text_color), + fontSize = 12.sp + ) } } } diff --git a/app/src/main/java/com/nextcloud/client/assistant/taskTypes/TaskTypesRow.kt b/app/src/main/java/com/nextcloud/client/assistant/taskTypes/TaskTypesRow.kt index fa99d737623f..3e0e9ad05d5f 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/taskTypes/TaskTypesRow.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/taskTypes/TaskTypesRow.kt @@ -8,10 +8,9 @@ package com.nextcloud.client.assistant.taskTypes import android.annotation.SuppressLint -import androidx.compose.material3.ScrollableTabRow +import androidx.compose.material3.PrimaryScrollableTabRow import androidx.compose.material3.Tab import androidx.compose.material3.TabRowDefaults -import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -26,13 +25,13 @@ import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData fun TaskTypesRow(selectedTaskType: TaskTypeData?, data: List, selectTaskType: (TaskTypeData) -> Unit) { val selectedTabIndex = data.indexOfFirst { it.id == selectedTaskType?.id }.takeIf { it >= 0 } ?: 0 - ScrollableTabRow( + PrimaryScrollableTabRow( selectedTabIndex = selectedTabIndex, edgePadding = 0.dp, containerColor = colorResource(R.color.actionbar_color), indicator = { TabRowDefaults.SecondaryIndicator( - Modifier.tabIndicatorOffset(it[selectedTabIndex]), + Modifier.tabIndicatorOffset(selectedTabIndex), color = colorResource(R.color.primary) ) } diff --git a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt index 821342964201..87ce9c999c68 100644 --- a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt +++ b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt @@ -16,6 +16,7 @@ import androidx.room.TypeConverters import com.nextcloud.client.core.Clock import com.nextcloud.client.core.ClockImpl import com.nextcloud.client.database.dao.ArbitraryDataDao +import com.nextcloud.client.database.dao.AssistantDao import com.nextcloud.client.database.dao.FileDao import com.nextcloud.client.database.dao.FileSystemDao import com.nextcloud.client.database.dao.OfflineOperationDao @@ -23,6 +24,7 @@ import com.nextcloud.client.database.dao.RecommendedFileDao import com.nextcloud.client.database.dao.SyncedFolderDao import com.nextcloud.client.database.dao.UploadDao import com.nextcloud.client.database.entity.ArbitraryDataEntity +import com.nextcloud.client.database.entity.AssistantEntity import com.nextcloud.client.database.entity.CapabilityEntity import com.nextcloud.client.database.entity.ExternalLinkEntity import com.nextcloud.client.database.entity.FileEntity @@ -39,6 +41,7 @@ import com.nextcloud.client.database.migrations.Migration67to68 import com.nextcloud.client.database.migrations.RoomMigration import com.nextcloud.client.database.migrations.addLegacyMigrations import com.nextcloud.client.database.typeConverter.OfflineOperationTypeConverter +import com.owncloud.android.MainApp import com.owncloud.android.db.ProviderMeta @Database( @@ -53,7 +56,8 @@ import com.owncloud.android.db.ProviderMeta UploadEntity::class, VirtualEntity::class, OfflineOperationEntity::class, - RecommendedFileEntity::class + RecommendedFileEntity::class, + AssistantEntity::class ], version = ProviderMeta.DB_VERSION, autoMigrations = [ @@ -85,7 +89,8 @@ import com.owncloud.android.db.ProviderMeta AutoMigration(from = 91, to = 92), AutoMigration(from = 92, to = 93, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class), AutoMigration(from = 93, to = 94, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class), - AutoMigration(from = 94, to = 95, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class) + AutoMigration(from = 94, to = 95, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class), + AutoMigration(from = 95, to = 96) ], exportSchema = true ) @@ -100,6 +105,7 @@ abstract class NextcloudDatabase : RoomDatabase() { abstract fun recommendedFileDao(): RecommendedFileDao abstract fun fileSystemDao(): FileSystemDao abstract fun syncedFolderDao(): SyncedFolderDao + abstract fun assistantDao(): AssistantDao companion object { const val FIRST_ROOM_DB_VERSION = 65 @@ -125,5 +131,9 @@ abstract class NextcloudDatabase : RoomDatabase() { } return instance!! } + + @Suppress("DEPRECATION") + @JvmStatic + fun instance(): NextcloudDatabase = getInstance(MainApp.getAppContext()) } } diff --git a/app/src/main/java/com/nextcloud/client/database/dao/AssistantDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/AssistantDao.kt new file mode 100644 index 000000000000..9ba9012ea2a6 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/database/dao/AssistantDao.kt @@ -0,0 +1,41 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.database.dao + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update +import com.nextcloud.client.database.entity.AssistantEntity +import com.owncloud.android.db.ProviderMeta + +@Dao +interface AssistantDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAssistantTask(task: AssistantEntity) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAssistantTasks(tasks: List) + + @Update + suspend fun updateAssistantTask(task: AssistantEntity) + + @Delete + suspend fun deleteAssistantTask(task: AssistantEntity) + + @Query( + """ + SELECT * FROM ${ProviderMeta.ProviderTableMeta.ASSISTANT_TABLE_NAME} + WHERE accountName = :accountName + ORDER BY lastUpdated DESC +""" + ) + suspend fun getAssistantTasksByAccount(accountName: String): List +} diff --git a/app/src/main/java/com/nextcloud/client/database/entity/AssistantEntity.kt b/app/src/main/java/com/nextcloud/client/database/entity/AssistantEntity.kt new file mode 100644 index 000000000000..4c13c25b340c --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/database/entity/AssistantEntity.kt @@ -0,0 +1,30 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.database.entity + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.owncloud.android.db.ProviderMeta + +@Entity(tableName = ProviderMeta.ProviderTableMeta.ASSISTANT_TABLE_NAME) +data class AssistantEntity( + @PrimaryKey(autoGenerate = true) + val id: Long = 0L, + val accountName: String?, + val type: String?, + val status: String?, + val userId: String?, + val appId: String?, + val input: String? = null, + val output: String? = null, + val completionExpectedAt: Int? = null, + var progress: Int? = null, + val lastUpdated: Int? = null, + val scheduledAt: Int? = null, + val endedAt: Int? = null +) diff --git a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt index 125f70093dbd..61cd04ec7e17 100644 --- a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt +++ b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt @@ -18,7 +18,9 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import com.nextcloud.client.assistant.AssistantScreen import com.nextcloud.client.assistant.AssistantViewModel -import com.nextcloud.client.assistant.repository.AssistantRepository +import com.nextcloud.client.assistant.repository.local.AssistantLocalRepositoryImpl +import com.nextcloud.client.assistant.repository.remote.AssistantRemoteRepositoryImpl +import com.nextcloud.client.database.NextcloudDatabase import com.nextcloud.common.NextcloudClient import com.nextcloud.utils.extensions.getSerializableArgument import com.owncloud.android.R @@ -79,10 +81,14 @@ class ComposeActivity : DrawerActivity() { isChecked = true } + val dao = NextcloudDatabase.instance().assistantDao() + nextcloudClient?.let { client -> AssistantScreen( viewModel = AssistantViewModel( - repository = AssistantRepository(client, capabilities) + accountName = userAccountManager.user.accountName, + remoteRepository = AssistantRemoteRepositoryImpl(client, capabilities), + localRepository = AssistantLocalRepositoryImpl(dao) ), activity = this, capability = capabilities diff --git a/app/src/main/java/com/owncloud/android/db/ProviderMeta.java b/app/src/main/java/com/owncloud/android/db/ProviderMeta.java index bd57f2748b40..46905d69231d 100644 --- a/app/src/main/java/com/owncloud/android/db/ProviderMeta.java +++ b/app/src/main/java/com/owncloud/android/db/ProviderMeta.java @@ -23,7 +23,7 @@ */ public class ProviderMeta { public static final String DB_NAME = "filelist"; - public static final int DB_VERSION = 95; + public static final int DB_VERSION = 96; private ProviderMeta() { // No instance @@ -52,6 +52,7 @@ static public class ProviderTableMeta implements BaseColumns { public static final String EXTERNAL_LINKS_TABLE_NAME = "external_links"; public static final String ARBITRARY_DATA_TABLE_NAME = "arbitrary_data"; public static final String VIRTUAL_TABLE_NAME = "virtual"; + public static final String ASSISTANT_TABLE_NAME = "assistant"; public static final String FILESYSTEM_TABLE_NAME = "filesystem"; public static final String EDITORS_TABLE_NAME = "editors"; public static final String CREATORS_TABLE_NAME = "creators"; diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1ee682c51846..eed9cbfb4251 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -56,7 +56,7 @@ Unable to fetch task types, please check your internet connection. Unable to fetch task list, please check your internet connection. - Task list is empty. + Task list is empty. Check assistant app configuration. Assistant Loading task list… @@ -78,6 +78,8 @@ Input Output + This content was generated by AI and can make mistakes. + Recommended files Assistant