From 8ae2a4d8a398b64122f9b8ea3f3368fc5566d196 Mon Sep 17 00:00:00 2001 From: zaednasr <75589932+zaednasr@users.noreply.github.com> Date: Wed, 13 Nov 2024 21:57:07 +0100 Subject: [PATCH] history revamp paginated wip --- .../20.json | 753 ++++++++++++++++++ .../21.json | 753 ++++++++++++++++++ .../java/com/deniscerri/ytdl/MainActivity.kt | 4 +- .../com/deniscerri/ytdl/database/DBManager.kt | 4 +- .../deniscerri/ytdl/database/Migrations.kt | 15 + .../ytdl/database/dao/HistoryDao.kt | 37 + .../ytdl/database/models/CommandTemplate.kt | 4 +- .../ytdl/database/models/HistoryItem.kt | 2 + .../database/repository/HistoryRepository.kt | 23 + .../database/viewmodel/DownloadViewModel.kt | 2 +- .../database/viewmodel/HistoryViewModel.kt | 108 ++- .../deniscerri/ytdl/receiver/ShareActivity.kt | 3 - .../com/deniscerri/ytdl/ui/HomeFragment.kt | 3 +- .../ui/adapter/HistoryPaginatedAdapter.kt | 243 ++++++ .../downloadcard/DownloadCommandFragment.kt | 4 +- .../ytdl/ui/downloads/HistoryFragment.kt | 416 +++++----- .../more/settings/UpdateSettingsFragment.kt | 6 + .../com/deniscerri/ytdl/util/Extensions.kt | 42 +- .../java/com/deniscerri/ytdl/util/UiUtil.kt | 5 +- .../deniscerri/ytdl/work/DownloadWorker.kt | 11 +- .../res/layout/create_command_template.xml | 14 + app/src/main/res/values/strings.xml | 2 +- app/src/main/res/xml/advanced_preferences.xml | 45 +- app/src/main/res/xml/updating_preferences.xml | 72 +- 24 files changed, 2249 insertions(+), 322 deletions(-) create mode 100644 app/schemas/com.deniscerri.ytdl.database.DBManager/20.json create mode 100644 app/schemas/com.deniscerri.ytdl.database.DBManager/21.json create mode 100644 app/src/main/java/com/deniscerri/ytdl/ui/adapter/HistoryPaginatedAdapter.kt diff --git a/app/schemas/com.deniscerri.ytdl.database.DBManager/20.json b/app/schemas/com.deniscerri.ytdl.database.DBManager/20.json new file mode 100644 index 00000000..fcce47f8 --- /dev/null +++ b/app/schemas/com.deniscerri.ytdl.database.DBManager/20.json @@ -0,0 +1,753 @@ +{ + "formatVersion": 1, + "database": { + "version": 20, + "identityHash": "3be2d00eeed0d077b84c352d10f6628f", + "entities": [ + { + "tableName": "results", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `author` TEXT NOT NULL, `duration` TEXT NOT NULL, `thumb` TEXT NOT NULL, `website` TEXT NOT NULL, `playlistTitle` TEXT NOT NULL, `formats` TEXT NOT NULL, `urls` TEXT NOT NULL DEFAULT '', `chapters` TEXT, `playlistURL` TEXT DEFAULT '', `playlistIndex` INTEGER, `creationTime` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumb", + "columnName": "thumb", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "website", + "columnName": "website", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "playlistTitle", + "columnName": "playlistTitle", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "formats", + "columnName": "formats", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "chapters", + "columnName": "chapters", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "playlistURL", + "columnName": "playlistURL", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "playlistIndex", + "columnName": "playlistIndex", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "creationTime", + "columnName": "creationTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `author` TEXT NOT NULL, `duration` TEXT NOT NULL, `thumb` TEXT NOT NULL, `type` TEXT NOT NULL, `time` INTEGER NOT NULL, `downloadPath` TEXT NOT NULL, `website` TEXT NOT NULL, `format` TEXT NOT NULL, `filesize` INTEGER NOT NULL DEFAULT 0, `downloadId` INTEGER NOT NULL DEFAULT 0, `command` TEXT NOT NULL DEFAULT '')", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumb", + "columnName": "thumb", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "downloadPath", + "columnName": "downloadPath", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "website", + "columnName": "website", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "format", + "columnName": "format", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filesize", + "columnName": "filesize", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "downloadId", + "columnName": "downloadId", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "command", + "columnName": "command", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "downloads", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `author` TEXT NOT NULL, `thumb` TEXT NOT NULL, `duration` TEXT NOT NULL, `type` TEXT NOT NULL, `format` TEXT NOT NULL, `container` TEXT NOT NULL DEFAULT 'Default', `downloadSections` TEXT NOT NULL DEFAULT '', `allFormats` TEXT NOT NULL, `downloadPath` TEXT NOT NULL, `website` TEXT NOT NULL, `downloadSize` TEXT NOT NULL, `playlistTitle` TEXT NOT NULL, `audioPreferences` TEXT NOT NULL, `videoPreferences` TEXT NOT NULL, `extraCommands` TEXT NOT NULL DEFAULT '', `customFileNameTemplate` TEXT NOT NULL, `SaveThumb` INTEGER NOT NULL, `status` TEXT NOT NULL DEFAULT 'Queued', `downloadStartTime` INTEGER NOT NULL DEFAULT 0, `logID` INTEGER, `playlistURL` TEXT DEFAULT '', `playlistIndex` INTEGER, `incognito` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumb", + "columnName": "thumb", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "format", + "columnName": "format", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "container", + "columnName": "container", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'Default'" + }, + { + "fieldPath": "downloadSections", + "columnName": "downloadSections", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "allFormats", + "columnName": "allFormats", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "downloadPath", + "columnName": "downloadPath", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "website", + "columnName": "website", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "downloadSize", + "columnName": "downloadSize", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "playlistTitle", + "columnName": "playlistTitle", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "audioPreferences", + "columnName": "audioPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "videoPreferences", + "columnName": "videoPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "extraCommands", + "columnName": "extraCommands", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "customFileNameTemplate", + "columnName": "customFileNameTemplate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "SaveThumb", + "columnName": "SaveThumb", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'Queued'" + }, + { + "fieldPath": "downloadStartTime", + "columnName": "downloadStartTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "logID", + "columnName": "logID", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "playlistURL", + "columnName": "playlistURL", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "playlistIndex", + "columnName": "playlistIndex", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "incognito", + "columnName": "incognito", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "commandTemplates", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `useAsExtraCommand` INTEGER NOT NULL DEFAULT 0, `useAsExtraCommandAudio` INTEGER NOT NULL DEFAULT 1, `useAsExtraCommandVideo` INTEGER NOT NULL DEFAULT 1, `useAsExtraCommandDataFetching` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "useAsExtraCommand", + "columnName": "useAsExtraCommand", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "useAsExtraCommandAudio", + "columnName": "useAsExtraCommandAudio", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "useAsExtraCommandVideo", + "columnName": "useAsExtraCommandVideo", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "useAsExtraCommandDataFetching", + "columnName": "useAsExtraCommandDataFetching", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "searchHistory", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "query", + "columnName": "query", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "templateShortcuts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `content` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `content` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "logs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `format` TEXT NOT NULL, `downloadType` TEXT NOT NULL, `downloadTime` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "format", + "columnName": "format", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "downloadType", + "columnName": "downloadType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "downloadTime", + "columnName": "downloadTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "terminalDownloads", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `command` TEXT NOT NULL, `log` TEXT DEFAULT '')", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "command", + "columnName": "command", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "log", + "columnName": "log", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `downloadItemTemplate` TEXT NOT NULL, `everyNr` INTEGER NOT NULL, `everyCategory` TEXT NOT NULL, `everyTime` INTEGER NOT NULL, `weeklyConfig` TEXT, `monthlyConfig` TEXT, `status` TEXT NOT NULL, `startsTime` INTEGER NOT NULL, `endsDate` INTEGER NOT NULL DEFAULT 0, `endsAfterCount` INTEGER NOT NULL DEFAULT 0, `runCount` INTEGER NOT NULL DEFAULT 0, `getOnlyNewUploads` INTEGER NOT NULL DEFAULT 0, `retryMissingDownloads` INTEGER NOT NULL, `ignoredLinks` TEXT NOT NULL DEFAULT '[]', `alreadyProcessedLinks` TEXT NOT NULL, `syncWithSource` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "downloadItemTemplate", + "columnName": "downloadItemTemplate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "everyNr", + "columnName": "everyNr", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "everyCategory", + "columnName": "everyCategory", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "everyTime", + "columnName": "everyTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weeklyConfig", + "columnName": "weeklyConfig", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "monthlyConfig", + "columnName": "monthlyConfig", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "startsTime", + "columnName": "startsTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "endsDate", + "columnName": "endsDate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "endsAfterCount", + "columnName": "endsAfterCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "runCount", + "columnName": "runCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "getOnlyNewUploads", + "columnName": "getOnlyNewUploads", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "retryMissingDownloads", + "columnName": "retryMissingDownloads", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ignoredLinks", + "columnName": "ignoredLinks", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'[]'" + }, + { + "fieldPath": "alreadyProcessedLinks", + "columnName": "alreadyProcessedLinks", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "syncWithSource", + "columnName": "syncWithSource", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "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, '3be2d00eeed0d077b84c352d10f6628f')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.deniscerri.ytdl.database.DBManager/21.json b/app/schemas/com.deniscerri.ytdl.database.DBManager/21.json new file mode 100644 index 00000000..e1a65e77 --- /dev/null +++ b/app/schemas/com.deniscerri.ytdl.database.DBManager/21.json @@ -0,0 +1,753 @@ +{ + "formatVersion": 1, + "database": { + "version": 21, + "identityHash": "3be2d00eeed0d077b84c352d10f6628f", + "entities": [ + { + "tableName": "results", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `author` TEXT NOT NULL, `duration` TEXT NOT NULL, `thumb` TEXT NOT NULL, `website` TEXT NOT NULL, `playlistTitle` TEXT NOT NULL, `formats` TEXT NOT NULL, `urls` TEXT NOT NULL DEFAULT '', `chapters` TEXT, `playlistURL` TEXT DEFAULT '', `playlistIndex` INTEGER, `creationTime` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumb", + "columnName": "thumb", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "website", + "columnName": "website", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "playlistTitle", + "columnName": "playlistTitle", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "formats", + "columnName": "formats", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "chapters", + "columnName": "chapters", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "playlistURL", + "columnName": "playlistURL", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "playlistIndex", + "columnName": "playlistIndex", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "creationTime", + "columnName": "creationTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `author` TEXT NOT NULL, `duration` TEXT NOT NULL, `thumb` TEXT NOT NULL, `type` TEXT NOT NULL, `time` INTEGER NOT NULL, `downloadPath` TEXT NOT NULL, `website` TEXT NOT NULL, `format` TEXT NOT NULL, `filesize` INTEGER NOT NULL DEFAULT 0, `downloadId` INTEGER NOT NULL DEFAULT 0, `command` TEXT NOT NULL DEFAULT '')", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumb", + "columnName": "thumb", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "downloadPath", + "columnName": "downloadPath", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "website", + "columnName": "website", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "format", + "columnName": "format", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filesize", + "columnName": "filesize", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "downloadId", + "columnName": "downloadId", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "command", + "columnName": "command", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "downloads", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `author` TEXT NOT NULL, `thumb` TEXT NOT NULL, `duration` TEXT NOT NULL, `type` TEXT NOT NULL, `format` TEXT NOT NULL, `container` TEXT NOT NULL DEFAULT 'Default', `downloadSections` TEXT NOT NULL DEFAULT '', `allFormats` TEXT NOT NULL, `downloadPath` TEXT NOT NULL, `website` TEXT NOT NULL, `downloadSize` TEXT NOT NULL, `playlistTitle` TEXT NOT NULL, `audioPreferences` TEXT NOT NULL, `videoPreferences` TEXT NOT NULL, `extraCommands` TEXT NOT NULL DEFAULT '', `customFileNameTemplate` TEXT NOT NULL, `SaveThumb` INTEGER NOT NULL, `status` TEXT NOT NULL DEFAULT 'Queued', `downloadStartTime` INTEGER NOT NULL DEFAULT 0, `logID` INTEGER, `playlistURL` TEXT DEFAULT '', `playlistIndex` INTEGER, `incognito` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumb", + "columnName": "thumb", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "format", + "columnName": "format", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "container", + "columnName": "container", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'Default'" + }, + { + "fieldPath": "downloadSections", + "columnName": "downloadSections", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "allFormats", + "columnName": "allFormats", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "downloadPath", + "columnName": "downloadPath", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "website", + "columnName": "website", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "downloadSize", + "columnName": "downloadSize", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "playlistTitle", + "columnName": "playlistTitle", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "audioPreferences", + "columnName": "audioPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "videoPreferences", + "columnName": "videoPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "extraCommands", + "columnName": "extraCommands", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "customFileNameTemplate", + "columnName": "customFileNameTemplate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "SaveThumb", + "columnName": "SaveThumb", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'Queued'" + }, + { + "fieldPath": "downloadStartTime", + "columnName": "downloadStartTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "logID", + "columnName": "logID", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "playlistURL", + "columnName": "playlistURL", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "playlistIndex", + "columnName": "playlistIndex", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "incognito", + "columnName": "incognito", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "commandTemplates", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `useAsExtraCommand` INTEGER NOT NULL DEFAULT 0, `useAsExtraCommandAudio` INTEGER NOT NULL DEFAULT 1, `useAsExtraCommandVideo` INTEGER NOT NULL DEFAULT 1, `useAsExtraCommandDataFetching` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "useAsExtraCommand", + "columnName": "useAsExtraCommand", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "useAsExtraCommandAudio", + "columnName": "useAsExtraCommandAudio", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "useAsExtraCommandVideo", + "columnName": "useAsExtraCommandVideo", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "useAsExtraCommandDataFetching", + "columnName": "useAsExtraCommandDataFetching", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "searchHistory", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "query", + "columnName": "query", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "templateShortcuts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `content` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `content` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "logs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `format` TEXT NOT NULL, `downloadType` TEXT NOT NULL, `downloadTime` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "format", + "columnName": "format", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "downloadType", + "columnName": "downloadType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "downloadTime", + "columnName": "downloadTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "terminalDownloads", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `command` TEXT NOT NULL, `log` TEXT DEFAULT '')", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "command", + "columnName": "command", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "log", + "columnName": "log", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `downloadItemTemplate` TEXT NOT NULL, `everyNr` INTEGER NOT NULL, `everyCategory` TEXT NOT NULL, `everyTime` INTEGER NOT NULL, `weeklyConfig` TEXT, `monthlyConfig` TEXT, `status` TEXT NOT NULL, `startsTime` INTEGER NOT NULL, `endsDate` INTEGER NOT NULL DEFAULT 0, `endsAfterCount` INTEGER NOT NULL DEFAULT 0, `runCount` INTEGER NOT NULL DEFAULT 0, `getOnlyNewUploads` INTEGER NOT NULL DEFAULT 0, `retryMissingDownloads` INTEGER NOT NULL, `ignoredLinks` TEXT NOT NULL DEFAULT '[]', `alreadyProcessedLinks` TEXT NOT NULL, `syncWithSource` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "downloadItemTemplate", + "columnName": "downloadItemTemplate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "everyNr", + "columnName": "everyNr", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "everyCategory", + "columnName": "everyCategory", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "everyTime", + "columnName": "everyTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weeklyConfig", + "columnName": "weeklyConfig", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "monthlyConfig", + "columnName": "monthlyConfig", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "startsTime", + "columnName": "startsTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "endsDate", + "columnName": "endsDate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "endsAfterCount", + "columnName": "endsAfterCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "runCount", + "columnName": "runCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "getOnlyNewUploads", + "columnName": "getOnlyNewUploads", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "retryMissingDownloads", + "columnName": "retryMissingDownloads", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ignoredLinks", + "columnName": "ignoredLinks", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'[]'" + }, + { + "fieldPath": "alreadyProcessedLinks", + "columnName": "alreadyProcessedLinks", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "syncWithSource", + "columnName": "syncWithSource", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "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, '3be2d00eeed0d077b84c352d10f6628f')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/MainActivity.kt b/app/src/main/java/com/deniscerri/ytdl/MainActivity.kt index f179a9e6..452cfeac 100644 --- a/app/src/main/java/com/deniscerri/ytdl/MainActivity.kt +++ b/app/src/main/java/com/deniscerri/ytdl/MainActivity.kt @@ -52,8 +52,6 @@ import com.deniscerri.ytdl.ui.downloads.DownloadQueueMainFragment import com.deniscerri.ytdl.ui.downloads.HistoryFragment import com.deniscerri.ytdl.ui.more.settings.SettingsActivity import com.deniscerri.ytdl.util.CrashListener -import com.deniscerri.ytdl.util.Extensions.extractURL -import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.NavbarUtil import com.deniscerri.ytdl.util.NavbarUtil.applyNavBarStyle import com.deniscerri.ytdl.util.ThemeUtil @@ -440,7 +438,7 @@ class MainActivity : BaseActivity() { textBuilder.append(c.toChar()) } val bundle = Bundle() - bundle.putString("url", textBuilder.toString().extractURL()) + bundle.putString("url", textBuilder.toString()) navController.popBackStack(R.id.homeFragment, true) navController.navigate( R.id.homeFragment, diff --git a/app/src/main/java/com/deniscerri/ytdl/database/DBManager.kt b/app/src/main/java/com/deniscerri/ytdl/database/DBManager.kt index 53273d66..fc843090 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/DBManager.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/DBManager.kt @@ -41,7 +41,7 @@ import com.deniscerri.ytdl.database.models.TerminalItem TerminalItem::class, ObserveSourcesItem::class ], - version = 19, + version = 21, autoMigrations = [ AutoMigration (from = 1, to = 2), AutoMigration (from = 2, to = 3), @@ -61,6 +61,8 @@ import com.deniscerri.ytdl.database.models.TerminalItem AutoMigration (from = 16, to = 17), AutoMigration (from = 17, to = 18), AutoMigration (from = 18, to = 19), + AutoMigration (from = 19, to = 20), + //AutoMigration (from = 20, to = 21) MANUALLY HANDLED ] ) abstract class DBManager : RoomDatabase(){ diff --git a/app/src/main/java/com/deniscerri/ytdl/database/Migrations.kt b/app/src/main/java/com/deniscerri/ytdl/database/Migrations.kt index cf702ef6..bbbef3aa 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/Migrations.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/Migrations.kt @@ -5,6 +5,8 @@ import androidx.room.DeleteTable import androidx.room.migration.AutoMigrationSpec import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +import com.deniscerri.ytdl.database.models.Format +import com.google.gson.Gson object Migrations { @@ -28,6 +30,19 @@ object Migrations { // Migration(17, 18 ){ database -> // database.execSQL("ALTER TABLE `sources` ADD COLUMN `syncWithSource` INTEGER NOT NULL DEFAULT 0") // } + + //add filesizes to history + Migration(20, 21) { database -> + val cursor = database.query("SELECT * FROM history") + while(cursor.moveToNext()) { + kotlin.runCatching { + val id = cursor.getLong(cursor.getColumnIndex("id")) + val format = cursor.getString(cursor.getColumnIndex("format")) + val parsed = Gson().fromJson(format, Format::class.java) + database.execSQL("UPDATE history SET filesize = ${parsed.filesize} WHERE id = $id") + } + } + } ) @DeleteTable.Entries( diff --git a/app/src/main/java/com/deniscerri/ytdl/database/dao/HistoryDao.kt b/app/src/main/java/com/deniscerri/ytdl/database/dao/HistoryDao.kt index f9fab094..75b66635 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/dao/HistoryDao.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/dao/HistoryDao.kt @@ -1,10 +1,12 @@ package com.deniscerri.ytdl.database.dao +import androidx.paging.PagingSource import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Update +import com.deniscerri.ytdl.database.models.DownloadItem import com.deniscerri.ytdl.database.models.HistoryItem import kotlinx.coroutines.flow.Flow @@ -30,9 +32,44 @@ interface HistoryDao { fun getHistorySortedByAuthor(query : String, type : String, site : String, sort : String) : List + + @Query("SELECT * FROM history WHERE (title LIKE '%'||:query||'%' OR author LIKE '%'||:query||'%') AND type LIKE '%'||:type||'%' AND website LIKE '%'||:site||'%' ORDER BY " + + "CASE WHEN :sort = 'ASC' THEN id END ASC," + + "CASE WHEN :sort = 'DESC' THEN id END DESC," + + "CASE WHEN :sort = '' THEN id END DESC ") + fun getHistorySortedByIDPaginated(query : String, type : String, site : String, sort : String) : PagingSource + + @Query("SELECT * FROM history WHERE (title LIKE '%'||:query||'%' OR author LIKE '%'||:query||'%') AND type LIKE '%'||:type||'%' AND website LIKE '%'||:site||'%' ORDER BY " + + "CASE WHEN :sort = 'ASC' THEN title END ASC," + + "CASE WHEN :sort = 'DESC' THEN title END DESC," + + "CASE WHEN :sort = '' THEN title END DESC ") + fun getHistorySortedByTitlePaginated(query : String, type : String, site : String, sort : String) : PagingSource + + @Query("SELECT * FROM history WHERE (title LIKE '%'||:query||'%' OR author LIKE '%'||:query||'%') AND type LIKE '%'||:type||'%' AND website LIKE '%'||:site||'%' ORDER BY " + + "CASE WHEN :sort = 'ASC' THEN author END ASC," + + "CASE WHEN :sort = 'DESC' THEN author END DESC," + + "CASE WHEN :sort = '' THEN author END DESC ") + fun getHistorySortedByAuthorPaginated(query : String, type : String, site : String, sort : String) : PagingSource + + @Query("SELECT * FROM history WHERE (title LIKE '%'||:query||'%' OR author LIKE '%'||:query||'%') AND type LIKE '%'||:type||'%' AND website LIKE '%'||:site||'%' ORDER BY " + + "CASE WHEN :sort = 'ASC' THEN filesize END ASC," + + "CASE WHEN :sort = 'DESC' THEN filesize END DESC," + + "CASE WHEN :sort = '' THEN filesize END DESC ") + fun getHistorySortedByFilesizePaginated(query : String, type : String, site : String, sort : String) : PagingSource + + @Query("SELECT * FROM history") fun getAllHistory() : Flow> + @Query("SELECT DISTINCT website FROM history") + fun getWebsites() : Flow> + + @Query("SELECT COUNT(*) FROM history") + fun getCount() : Flow + + @Query("SELECT * FROM history") + fun getAllHistoryPaginated() : PagingSource + @Query("SELECT * FROM history") fun getAllHistoryList() : List diff --git a/app/src/main/java/com/deniscerri/ytdl/database/models/CommandTemplate.kt b/app/src/main/java/com/deniscerri/ytdl/database/models/CommandTemplate.kt index 4003cda6..933de0d6 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/models/CommandTemplate.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/models/CommandTemplate.kt @@ -17,5 +17,7 @@ data class CommandTemplate( @ColumnInfo(defaultValue = "1") var useAsExtraCommandAudio: Boolean, @ColumnInfo(defaultValue = "1") - var useAsExtraCommandVideo: Boolean + var useAsExtraCommandVideo: Boolean, + @ColumnInfo(defaultValue = "0") + var useAsExtraCommandDataFetching: Boolean ) \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/database/models/HistoryItem.kt b/app/src/main/java/com/deniscerri/ytdl/database/models/HistoryItem.kt index 097d7ba0..c9785bd9 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/models/HistoryItem.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/models/HistoryItem.kt @@ -20,6 +20,8 @@ data class HistoryItem( val website: String, val format: Format, @ColumnInfo(defaultValue = "0") + val filesize: Long = 0, + @ColumnInfo(defaultValue = "0") val downloadId: Long, @ColumnInfo(defaultValue = "") val command: String = "" diff --git a/app/src/main/java/com/deniscerri/ytdl/database/repository/HistoryRepository.kt b/app/src/main/java/com/deniscerri/ytdl/database/repository/HistoryRepository.kt index 2ade553c..7799bc40 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/repository/HistoryRepository.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/repository/HistoryRepository.kt @@ -1,5 +1,10 @@ package com.deniscerri.ytdl.database.repository +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.PagingSource +import androidx.paging.filter import com.deniscerri.ytdl.database.DBManager.SORTING import com.deniscerri.ytdl.database.dao.HistoryDao import com.deniscerri.ytdl.database.models.HistoryItem @@ -7,9 +12,12 @@ import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel import com.deniscerri.ytdl.util.FileUtil import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.map class HistoryRepository(private val historyDao: HistoryDao) { val items : Flow> = historyDao.getAllHistory() + val websites : Flow> = historyDao.getWebsites() + val count : Flow = historyDao.getCount() enum class HistorySortType { DATE, TITLE, AUTHOR, FILESIZE @@ -27,6 +35,7 @@ class HistoryRepository(private val historyDao: HistoryDao) { return historyDao.getAllHistoryByURL(url) } + fun getFiltered(query : String, type : String, site : String, sortType: HistorySortType, sort: SORTING, statusFilter: HistoryViewModel.HistoryStatus) : List { var filtered = when(sortType){ HistorySortType.DATE -> historyDao.getHistorySortedByID(query, type, site, sort.toString()) @@ -54,6 +63,20 @@ class HistoryRepository(private val historyDao: HistoryDao) { return filtered } + fun getPaginatedSource(query : String, type : String, site : String, sortType: HistorySortType, sort: SORTING) : PagingSource { + val source = when(sortType){ + HistorySortType.DATE -> historyDao.getHistorySortedByIDPaginated(query, type, site, sort.toString()) + HistorySortType.TITLE -> historyDao.getHistorySortedByTitlePaginated(query, type, site, sort.toString()) + HistorySortType.AUTHOR -> historyDao.getHistorySortedByAuthorPaginated(query, type, site, sort.toString()) + HistorySortType.FILESIZE -> { + historyDao.getHistorySortedByFilesizePaginated(query, type, site, sort.toString()) + } + } + + return source + } + + suspend fun insert(item: HistoryItem){ historyDao.insert(item) } diff --git a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt index 97d338f8..ee2216e9 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt @@ -550,7 +550,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel } return generateCommandFormat( - c ?: CommandTemplate(0, "", preferredTemplateContent, useAsExtraCommand = false, useAsExtraCommandAudio = false, useAsExtraCommandVideo = false) + c ?: CommandTemplate(0, "", preferredTemplateContent, useAsExtraCommand = false, useAsExtraCommandAudio = false, useAsExtraCommandVideo = false, useAsExtraCommandDataFetching = false) ) } } diff --git a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/HistoryViewModel.kt b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/HistoryViewModel.kt index 479bba0a..be6e374d 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/HistoryViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/HistoryViewModel.kt @@ -6,68 +6,109 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.asLiveData +import androidx.lifecycle.switchMap import androidx.lifecycle.viewModelScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.cachedIn +import androidx.paging.filter import com.deniscerri.ytdl.database.DBManager import com.deniscerri.ytdl.database.DBManager.SORTING import com.deniscerri.ytdl.database.models.HistoryItem -import com.deniscerri.ytdl.database.repository.DownloadRepository import com.deniscerri.ytdl.database.repository.HistoryRepository import com.deniscerri.ytdl.database.repository.HistoryRepository.HistorySortType import com.deniscerri.ytdl.util.Extensions +import com.deniscerri.ytdl.util.FileUtil import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.cache import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flattenMerge +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.merge import kotlinx.coroutines.launch +@OptIn(ExperimentalCoroutinesApi::class) class HistoryViewModel(application: Application) : AndroidViewModel(application) { private val repository : HistoryRepository - val sortOrder = MutableLiveData(SORTING.DESC) - val sortType = MutableLiveData(HistorySortType.DATE) - val websiteFilter = MutableLiveData("") - val statusFilter = MutableLiveData(HistoryStatus.ALL) - private val queryFilter = MutableLiveData("") - private val typeFilter = MutableLiveData("") + val sortOrder = MutableStateFlow(SORTING.DESC) + val sortType = MutableStateFlow(HistorySortType.DATE) + val websiteFilter = MutableStateFlow("") + val statusFilter = MutableStateFlow(HistoryStatus.ALL) + private val queryFilter = MutableStateFlow("") + private val typeFilter = MutableStateFlow("") enum class HistoryStatus { UNSET, DELETED, NOT_DELETED, ALL } - val allItems : LiveData> private var _items = MediatorLiveData>() + var paginatedItems : Flow> + var websites : Flow> + var totalCount : Flow + + data class HistoryFilters( + var type: String = "", + var sortType: HistorySortType = HistorySortType.DATE, + var sortOrder: SORTING = SORTING.DESC, + var query: String = "", + var status: HistoryStatus = HistoryStatus.ALL, + var website: String = "" + ) init { val dao = DBManager.getInstance(application).historyDao repository = HistoryRepository(dao) - allItems = repository.items.asLiveData() - _items.addSource(allItems){ - filter(queryFilter.value!!, typeFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, statusFilter.value!!) - } - _items.addSource(typeFilter){ - filter(queryFilter.value!!, typeFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, statusFilter.value!!) - } - _items.addSource(sortType){ - filter(queryFilter.value!!, typeFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, statusFilter.value!!) - } - _items.addSource(websiteFilter){ - filter(queryFilter.value!!, typeFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, statusFilter.value!!) - } - _items.addSource(queryFilter){ - filter(queryFilter.value!!, typeFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, statusFilter.value!!) - } + websites = repository.websites + totalCount = repository.count - _items.addSource(statusFilter){ - filter(queryFilter.value!!, typeFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, statusFilter.value!!) - } + val filters = listOf(dao.getAllHistory(), sortOrder, sortType, websiteFilter, statusFilter, queryFilter, typeFilter) + paginatedItems = combine(filters) { f -> + val sortOrder = f[1] as SORTING + val sortType = f[2] as HistorySortType + val website = f[3] as String + val status = f[4] as HistoryStatus + val query = f[5] as String + val type = f[6] as String - } + var pager = Pager( + config = PagingConfig(pageSize = 20, initialLoadSize = 20, prefetchDistance = 1), + pagingSourceFactory = { + repository.getPaginatedSource(query, type, website, sortType, sortOrder) + } + ).flow - fun getFilteredList() : LiveData>{ - return _items + when(status) { + HistoryStatus.DELETED -> { + pager = pager.map { + it.filter { it2 -> + it2.downloadPath.any { it3 -> + !FileUtil.exists(it3) + } + } + } + } + HistoryStatus.NOT_DELETED -> { + pager = pager.map { + it.filter { it2 -> + it2.downloadPath.any { it3 -> + FileUtil.exists(it3) + } + } + } + } + else -> {} + } + + pager + }.flatMapLatest { it } } fun setSorting(sort: HistorySortType){ @@ -98,7 +139,8 @@ class HistoryViewModel(application: Application) : AndroidViewModel(application) } private fun filter(query : String, format : String, site : String, sortType: HistorySortType, sort: SORTING, statusFilter: HistoryStatus) = viewModelScope.launch(Dispatchers.IO){ - _items.postValue(repository.getFiltered(query, format, site, sortType, sort, statusFilter)) + + } fun getAll() : List { diff --git a/app/src/main/java/com/deniscerri/ytdl/receiver/ShareActivity.kt b/app/src/main/java/com/deniscerri/ytdl/receiver/ShareActivity.kt index 7788ee99..6687f571 100644 --- a/app/src/main/java/com/deniscerri/ytdl/receiver/ShareActivity.kt +++ b/app/src/main/java/com/deniscerri/ytdl/receiver/ShareActivity.kt @@ -41,8 +41,6 @@ import com.deniscerri.ytdl.database.viewmodel.ResultViewModel import com.deniscerri.ytdl.ui.BaseActivity import com.deniscerri.ytdl.util.Extensions.extractURL import com.deniscerri.ytdl.util.ThemeUtil -import com.deniscerri.ytdl.util.UiUtil -import com.google.android.material.dialog.MaterialAlertDialogBuilder import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -50,7 +48,6 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlin.properties.Delegates -import kotlin.system.exitProcess class ShareActivity : BaseActivity() { diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/HomeFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/HomeFragment.kt index 18b4f5f9..48b7d27d 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/HomeFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/HomeFragment.kt @@ -45,6 +45,7 @@ import com.deniscerri.ytdl.ui.adapter.SearchSuggestionsAdapter import com.deniscerri.ytdl.ui.downloads.HistoryFragment import com.deniscerri.ytdl.ui.downloads.HistoryFragment.Companion import com.deniscerri.ytdl.util.Extensions.enableFastScroll +import com.deniscerri.ytdl.util.Extensions.isURL import com.deniscerri.ytdl.util.NotificationUtil import com.deniscerri.ytdl.util.ThemeUtil import com.deniscerri.ytdl.util.UiUtil @@ -231,7 +232,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti val url = requireArguments().getString("url") if (inputQueries == null) inputQueries = mutableListOf() searchBar?.setText(url) - val argList = url!!.split("\n").toMutableList() + val argList = url!!.split("\n").filter { it.isURL() }.toMutableList() argList.removeAll(listOf("", null)) inputQueries!!.addAll(argList) } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/HistoryPaginatedAdapter.kt b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/HistoryPaginatedAdapter.kt new file mode 100644 index 00000000..6888c8a9 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/HistoryPaginatedAdapter.kt @@ -0,0 +1,243 @@ +package com.deniscerri.ytdl.ui.adapter + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.SharedPreferences +import android.graphics.ColorMatrix +import android.graphics.ColorMatrixColorFilter +import android.os.Handler +import android.os.Looper +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.paging.PagingDataAdapter +import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.AsyncDifferConfig +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.deniscerri.ytdl.R +import com.deniscerri.ytdl.database.models.HistoryItem +import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel +import com.deniscerri.ytdl.util.Extensions.loadThumbnail +import com.deniscerri.ytdl.util.Extensions.popup +import com.google.android.material.card.MaterialCardView +import com.google.android.material.color.MaterialColors +import com.google.android.material.floatingactionbutton.FloatingActionButton +import java.io.File +import java.text.SimpleDateFormat +import java.util.* + + +class HistoryPaginatedAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : PagingDataAdapter( + DIFF_CALLBACK +) { + val checkedItems: MutableSet + var inverted: Boolean + private val onItemClickListener: OnItemClickListener + private val activity: Activity + private val sharedPreferences: SharedPreferences + + init { + checkedItems = mutableSetOf() + this.inverted = false + this.onItemClickListener = onItemClickListener + this.activity = activity + sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity) + } + + class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) { + val cardView: MaterialCardView + + init { + cardView = itemView.findViewById(R.id.downloads_card_view) + } + } + + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HistoryPaginatedAdapter.ViewHolder { + return if (viewType == 0){ + val cardView = LayoutInflater.from(parent.context) + .inflate(R.layout.history_card, parent, false) + ViewHolder(cardView, onItemClickListener) + }else{ + val cardView = LayoutInflater.from(parent.context) + .inflate(R.layout.history_card_multiple, parent, false) + ViewHolder(cardView, onItemClickListener) + } + } + + override fun getItemViewType(position: Int): Int { + getItem(position)?.apply { + return if(this.downloadPath.size == 1){ + 0 + }else{ + 1 + } + } + return 0 + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = getItem(position) ?: return + val card = holder.cardView + card.tag = item.id.toString() + card.popup() + + + val uiHandler = Handler(Looper.getMainLooper()) + val thumbnail = card.findViewById(R.id.downloads_image_view) + + // THUMBNAIL ---------------------------------- + val hideThumb = sharedPreferences.getStringSet("hide_thumbnails", emptySet())!!.contains("downloads") + uiHandler.post { thumbnail.loadThumbnail(hideThumb, item!!.thumb) } + + // TITLE ---------------------------------- + val itemTitle = card.findViewById(R.id.downloads_title) + var title = item!!.title.ifEmpty { item.url } + if (title.length > 100) { + title = title.substring(0, 40) + "..." + } + itemTitle.text = title + + // Bottom Info ---------------------------------- + val author = card.findViewById(R.id.downloads_info_bottom) + author.text = item.author + + val length = card.findViewById(R.id.length) + length.text = if(item.downloadPath.size == 1) item.duration else "" + + + // TIME DOWNLOADED ---------------------------------- + val datetime = card.findViewById(R.id.downloads_info_time) + datetime.text = SimpleDateFormat(android.text.format.DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(item.time * 1000L) + + // BUTTON ---------------------------------- + val btn = card.findViewById(R.id.downloads_download_button_type) + var filesPresent = true + + //IS IN THE FILE SYSTEM? + if (item.downloadPath.all { !File(it).exists() && it.isNotBlank()}) { + filesPresent = false + thumbnail.colorFilter = ColorMatrixColorFilter(object : ColorMatrix() { + init { + setSaturation(0f) + } + }) + thumbnail.alpha = 0.7f + btn.backgroundTintList = MaterialColors.getColorStateList(activity, R.attr.colorSurface, ContextCompat.getColorStateList(activity, android.R.color.transparent)!!) + }else{ + thumbnail.alpha = 1f + thumbnail.colorFilter = null + btn.backgroundTintList = MaterialColors.getColorStateList(activity, R.attr.colorPrimaryContainer, ContextCompat.getColorStateList(activity, android.R.color.transparent)!!) + } + + if (item.type == DownloadViewModel.Type.audio) { + if (filesPresent) btn.setImageResource(R.drawable.ic_music_downloaded) else { + btn.setImageResource(R.drawable.ic_music) + } + } else if (item.type == DownloadViewModel.Type.video) { + if (filesPresent) btn.setImageResource(R.drawable.ic_video_downloaded) else btn.setImageResource(R.drawable.ic_video) + }else{ + if (filesPresent) btn.setImageResource(R.drawable.ic_terminal) else btn.setImageResource(R.drawable.baseline_code_off_24) + } + if (btn.hasOnClickListeners()) btn.setOnClickListener(null) + btn.isClickable = filesPresent + + if (checkedItems.contains(item.id)) { + card.isChecked = true + card.strokeWidth = 5 + } else { + card.isChecked = false + card.strokeWidth = 0 + } + val finalFilePresent = filesPresent + card.setOnLongClickListener { + checkCard(card, item.id) + true + } + card.setOnClickListener { + if (checkedItems.size > 0) { + checkCard(card, item.id) + } else { + onItemClickListener.onCardClick(item.id, finalFilePresent) + } + } + + btn.setOnClickListener { + onItemClickListener.onButtonClick(item.id, finalFilePresent) + } + } + + @SuppressLint("NotifyDataSetChanged") + fun clearCheckedItems() { + inverted = false + checkedItems.clear() + notifyDataSetChanged() + } + + @SuppressLint("NotifyDataSetChanged") + fun checkAll() { + checkedItems.clear() + inverted = true + notifyDataSetChanged() + } + + @SuppressLint("NotifyDataSetChanged") + fun checkMultipleItems(list: List){ + checkedItems.clear() + inverted = false + checkedItems.addAll(list) + notifyDataSetChanged() + } + + @SuppressLint("NotifyDataSetChanged") + fun invertSelected() { + inverted = !inverted + notifyDataSetChanged() + } + + fun getSelectedObjectsCount(totalSize: Int) : Int{ + return if (inverted){ + totalSize - checkedItems.size + }else{ + checkedItems.size + } + } + + private fun checkCard(card: MaterialCardView, itemID: Long) { + if (card.isChecked) { + card.strokeWidth = 0 + if (inverted) checkedItems.add(itemID) + else checkedItems.remove(itemID) + } else { + card.strokeWidth = 5 + if (inverted) checkedItems.remove(itemID) + else checkedItems.add(itemID) + } + card.isChecked = !card.isChecked + onItemClickListener.onCardSelect(itemID, card.isChecked) + } + + interface OnItemClickListener { + fun onCardClick(itemID: Long, isPresent: Boolean) + fun onButtonClick(itemID: Long, isPresent: Boolean) + fun onCardSelect(itemID: Long, isChecked: Boolean) + } + + companion object { + private val DIFF_CALLBACK: DiffUtil.ItemCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: HistoryItem, newItem: HistoryItem): Boolean { + val ranged = arrayListOf(oldItem.id, newItem.id) + return ranged[0] == ranged[1] + } + + override fun areContentsTheSame(oldItem: HistoryItem, newItem: HistoryItem): Boolean { + return oldItem.time == newItem.time + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadCommandFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadCommandFragment.kt index 040a9bf3..2816e10d 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadCommandFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadCommandFragment.kt @@ -93,7 +93,7 @@ class DownloadCommandFragment(private val resultItem: ResultItem? = null, privat preferences.edit().putString("lastCommandTemplateUsed", downloadItem.format.format_note).apply() if (!Patterns.WEB_URL.matcher(downloadItem.url).matches() && downloadItem.url.endsWith(".txt")){ - downloadItem.format = downloadViewModel.generateCommandFormat(CommandTemplate(0,"txt", "-a \"${downloadItem.url}\"", useAsExtraCommand = false, useAsExtraCommandAudio = false, useAsExtraCommandVideo = false)) + downloadItem.format = downloadViewModel.generateCommandFormat(CommandTemplate(0,"txt", "-a \"${downloadItem.url}\"", useAsExtraCommand = false, useAsExtraCommandAudio = false, useAsExtraCommandVideo = false, useAsExtraCommandDataFetching = false)) downloadItem.url = "" } @@ -249,7 +249,7 @@ class DownloadCommandFragment(private val resultItem: ResultItem? = null, privat 0, "", chosenCommandView.editText!!.text.toString(), - useAsExtraCommand = false, useAsExtraCommandAudio = false, useAsExtraCommandVideo = false + useAsExtraCommand = false, useAsExtraCommandAudio = false, useAsExtraCommandVideo = false, useAsExtraCommandDataFetching = false ) UiUtil.showCommandTemplateCreationOrUpdatingSheet( current, requireActivity(), viewLifecycleOwner, commandTemplateViewModel, diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloads/HistoryFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloads/HistoryFragment.kt index 64e22f34..b511a55c 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloads/HistoryFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloads/HistoryFragment.kt @@ -1,7 +1,6 @@ package com.deniscerri.ytdl.ui.downloads import android.annotation.SuppressLint -import android.app.Activity import android.content.Context import android.content.DialogInterface import android.graphics.Canvas @@ -16,8 +15,6 @@ import android.view.LayoutInflater import android.view.Menu import android.view.MenuItem import android.view.View -import android.view.View.GONE -import android.view.View.VISIBLE import android.view.ViewGroup import android.view.Window import android.widget.LinearLayout @@ -45,13 +42,12 @@ import com.deniscerri.ytdl.MainActivity import com.deniscerri.ytdl.R import com.deniscerri.ytdl.database.DBManager.SORTING import com.deniscerri.ytdl.database.models.HistoryItem +import com.deniscerri.ytdl.database.repository.DownloadRepository import com.deniscerri.ytdl.database.repository.HistoryRepository import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel -import com.deniscerri.ytdl.ui.adapter.HistoryAdapter -import com.deniscerri.ytdl.ui.downloadcard.FormatSelectionBottomSheetDialog.FormatCategory +import com.deniscerri.ytdl.ui.adapter.HistoryPaginatedAdapter import com.deniscerri.ytdl.util.Extensions.enableFastScroll -import com.deniscerri.ytdl.util.Extensions.isYoutubeURL import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.NavbarUtil import com.deniscerri.ytdl.util.UiUtil @@ -73,26 +69,23 @@ import kotlinx.coroutines.withContext /** * A fragment representing a list of Items. */ -class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{ +class HistoryFragment : Fragment(), HistoryPaginatedAdapter.OnItemClickListener{ private lateinit var historyViewModel : HistoryViewModel private lateinit var downloadViewModel : DownloadViewModel - private var fragmentView: View? = null - private var activity: Activity? = null + private lateinit var fragmentView: View private var mainActivity: MainActivity? = null private var fragmentContext: Context? = null - private var layoutinflater: LayoutInflater? = null - private var topAppBar: MaterialToolbar? = null + private lateinit var layoutinflater: LayoutInflater + private lateinit var topAppBar: MaterialToolbar private lateinit var recyclerView: RecyclerView - private var historyAdapter: HistoryAdapter? = null - private var sortSheet: BottomSheetDialog? = null - private var uiHandler: Handler? = null - private var noResults: RelativeLayout? = null - private var selectionChips: LinearLayout? = null - private var historyList: List? = null - private var allhistoryList: List? = null + private lateinit var historyAdapter: HistoryPaginatedAdapter + private lateinit var sortSheet: BottomSheetDialog + private lateinit var uiHandler: Handler + private lateinit var noResults: RelativeLayout + private lateinit var selectionChips: LinearLayout private var websiteList: MutableList = mutableListOf() - private lateinit var selectedObjects: ArrayList + private var totalCount = 0 private var actionMode : ActionMode? = null private lateinit var sortChip: Chip @@ -100,9 +93,8 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{ override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { + ): View { fragmentView = inflater.inflate(R.layout.fragment_history, container, false) - activity = getActivity() mainActivity = activity as MainActivity? return fragmentView } @@ -118,24 +110,15 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{ selectionChips = view.findViewById(R.id.history_selection_chips) uiHandler = Handler(Looper.getMainLooper()) sortChip = view.findViewById(R.id.sortChip) - selectedObjects = arrayListOf() val isInNavBar = NavbarUtil.getNavBarItems(requireActivity()).any { n -> n.itemId == R.id.historyFragment && n.isVisible } if (isInNavBar) { - topAppBar?.navigationIcon = null + topAppBar.navigationIcon = null }else{ mainActivity?.hideBottomNavigation() } - topAppBar?.setNavigationOnClickListener { mainActivity?.onBackPressedDispatcher?.onBackPressed() } - - - historyList = mutableListOf() - - historyAdapter = - HistoryAdapter( - this, - requireActivity() - ) + topAppBar.setNavigationOnClickListener { mainActivity?.onBackPressedDispatcher?.onBackPressed() } + historyAdapter = HistoryPaginatedAdapter(this, requireActivity()) recyclerView = view.findViewById(R.id.recyclerviewhistorys) recyclerView.enableFastScroll() @@ -146,37 +129,44 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{ } recyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size)) - noResults?.visibility = GONE - - + noResults.isVisible = false historyViewModel = ViewModelProvider(this)[HistoryViewModel::class.java] - historyViewModel.allItems.observe(viewLifecycleOwner) { - allhistoryList = it - if(it.isEmpty()){ - noResults!!.visibility = VISIBLE - selectionChips!!.visibility = GONE - topAppBar!!.menu.children.firstOrNull { m -> m.itemId == R.id.filters }?.isVisible = false - }else{ - noResults!!.visibility = GONE - selectionChips!!.visibility = VISIBLE - topAppBar!!.menu.children.firstOrNull { m -> m.itemId == R.id.filters }?.isVisible = true - updateWebsiteChips(it) + recyclerView.adapter = historyAdapter + lifecycleScope.launch { + historyViewModel.paginatedItems.collectLatest { + withContext(Dispatchers.IO){ + historyAdapter.submitData(it) + } + } + } + lifecycleScope.launch { + historyViewModel.websites.collectLatest { + if(it.isEmpty()) { + noResults.isVisible = true + selectionChips.isVisible = false + topAppBar.menu.children.firstOrNull { m -> m.itemId == R.id.filters }?.isVisible = false + }else{ + noResults.isVisible = false + selectionChips.isVisible = true + topAppBar.menu.children.firstOrNull { m -> m.itemId == R.id.filters }?.isVisible = true + + websiteList = mutableListOf() + for (item in it){ + if (item == "null" || item.isEmpty()) continue + if (!websiteList.any { it.contentEquals(item, true) }) websiteList.add(item) + } + } } } - historyViewModel.getFilteredList().observe(viewLifecycleOwner) { - historyAdapter!!.submitList(it) - historyList = it - - if (recyclerView.adapter == null){ - recyclerView.adapter = historyAdapter - }else{ - scrollToTop() + lifecycleScope.launch { + historyViewModel.totalCount.collectLatest { + totalCount = it } } - historyViewModel.sortOrder.observe(viewLifecycleOwner){ - if (it != null){ + lifecycleScope.launch { + historyViewModel.sortOrder.collectLatest { when(it){ SORTING.ASC -> sortChip.chipIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_down) SORTING.DESC -> sortChip.chipIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_up) @@ -184,20 +174,18 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{ } } - historyViewModel.sortType.observe(viewLifecycleOwner){ - if(it != null){ - when(it){ + lifecycleScope.launch { + historyViewModel.sortType.collectLatest { + when(it){ HistoryRepository.HistorySortType.AUTHOR -> sortChip.text = getString(R.string.author) HistoryRepository.HistorySortType.DATE -> sortChip.text = getString(R.string.date_added) HistoryRepository.HistorySortType.TITLE -> sortChip.text = getString(R.string.title) HistoryRepository.HistorySortType.FILESIZE -> sortChip.text = getString(R.string.file_size) - } } } + downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] - - lifecycleScope.launch{ downloadViewModel.alreadyExistsUiState.collectLatest { res -> if (res.isNotEmpty()){ @@ -212,7 +200,6 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{ } } - initMenu() initChips() } @@ -220,7 +207,7 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{ fun scrollToTop() { recyclerView.scrollToPosition(0) Handler(Looper.getMainLooper()).post { - (topAppBar!!.parent as AppBarLayout).setExpanded( + (topAppBar.parent as AppBarLayout).setExpanded( true, true ) @@ -238,14 +225,14 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{ return true } } - topAppBar!!.menu.findItem(R.id.search_history) + topAppBar.menu.findItem(R.id.search_history) .setOnActionExpandListener(onActionExpandListener) - val searchView = topAppBar!!.menu.findItem(R.id.search_history).actionView as SearchView? + val searchView = topAppBar.menu.findItem(R.id.search_history).actionView as SearchView? searchView!!.inputType = InputType.TYPE_CLASS_TEXT searchView.queryHint = getString(R.string.search_history_hint) searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String): Boolean { - topAppBar!!.menu.findItem(R.id.search_history).collapseActionView() + topAppBar.menu.findItem(R.id.search_history).collapseActionView() historyViewModel.setQueryFilter(query) return true } @@ -255,13 +242,13 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{ return true } }) - topAppBar!!.setOnClickListener { scrollToTop() } + topAppBar.setOnClickListener { scrollToTop() } val showingDownloadQueue = NavbarUtil.getNavBarItems(requireContext()).any { n -> n.itemId == R.id.downloadQueueMainFragment && n.isVisible } - topAppBar!!.menu.findItem(R.id.download_queue).isVisible = !showingDownloadQueue - topAppBar!!.setOnMenuItemClickListener { m: MenuItem -> + topAppBar.menu.findItem(R.id.download_queue).isVisible = !showingDownloadQueue + topAppBar.setOnMenuItemClickListener { m: MenuItem -> when (m.itemId) { R.id.remove_history -> { - if(allhistoryList!!.isEmpty()){ + if(websiteList.isEmpty()){ Toast.makeText(context, R.string.history_is_empty, Toast.LENGTH_SHORT).show() }else{ val deleteFile = booleanArrayOf(false) @@ -282,7 +269,7 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{ findNavController().navigate(R.id.downloadQueueMainFragment) } R.id.remove_deleted_history -> { - if(allhistoryList!!.isEmpty()){ + if(websiteList.isEmpty()){ Toast.makeText(context, R.string.history_is_empty, Toast.LENGTH_SHORT).show() }else{ val deleteDialog = MaterialAlertDialogBuilder(fragmentContext!!) @@ -296,7 +283,7 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{ } } R.id.remove_duplicates -> { - if(allhistoryList!!.isEmpty()){ + if(websiteList.isEmpty()){ Toast.makeText(context, R.string.history_is_empty, Toast.LENGTH_SHORT).show() }else{ val deleteDialog = MaterialAlertDialogBuilder(fragmentContext!!) @@ -384,7 +371,7 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{ for (i in websiteList.indices) { val w = websiteList[i] - val tmp = layoutinflater!!.inflate(R.layout.filter_chip, websiteGroup, false) as Chip + val tmp = layoutinflater.inflate(R.layout.filter_chip, websiteGroup, false) as Chip tmp.text = w tmp.id = i tmp.setOnClickListener { @@ -427,16 +414,16 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{ private fun initChips() { - val sortChip = fragmentView!!.findViewById(R.id.sortChip) + val sortChip = fragmentView.findViewById(R.id.sortChip) sortChip.setOnClickListener { sortSheet = BottomSheetDialog(requireContext()) - sortSheet!!.requestWindowFeature(Window.FEATURE_NO_TITLE) - sortSheet!!.setContentView(R.layout.history_sort_sheet) + sortSheet.requestWindowFeature(Window.FEATURE_NO_TITLE) + sortSheet.setContentView(R.layout.history_sort_sheet) - val date = sortSheet!!.findViewById(R.id.date) - val title = sortSheet!!.findViewById(R.id.title) - val author = sortSheet!!.findViewById(R.id.author) - val filesize = sortSheet!!.findViewById(R.id.filesize) + val date = sortSheet.findViewById(R.id.date) + val title = sortSheet.findViewById(R.id.title) + val author = sortSheet.findViewById(R.id.author) + val filesize = sortSheet.findViewById(R.id.filesize) val sortOptions = listOf(date!!, title!!, author!!, filesize!!) sortOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) } @@ -469,12 +456,12 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{ } val displayMetrics = DisplayMetrics() requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics) - sortSheet!!.behavior.peekHeight = displayMetrics.heightPixels - sortSheet!!.show() + sortSheet.behavior.peekHeight = displayMetrics.heightPixels + sortSheet.show() } //format - val audio = fragmentView!!.findViewById(R.id.audio_chip) + val audio = fragmentView.findViewById(R.id.audio_chip) audio.setOnClickListener { if (audio.isChecked) { historyViewModel.setTypeFilter(DownloadViewModel.Type.audio.toString()) @@ -484,7 +471,7 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{ audio.isChecked = false } } - val video = fragmentView!!.findViewById(R.id.video_chip) + val video = fragmentView.findViewById(R.id.video_chip) video.setOnClickListener { if (video.isChecked) { historyViewModel.setTypeFilter(DownloadViewModel.Type.video.toString()) @@ -494,7 +481,7 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{ video.isChecked = false } } - val command = fragmentView!!.findViewById(R.id.command_chip) + val command = fragmentView.findViewById(R.id.command_chip) command.setOnClickListener { if (command.isChecked) { historyViewModel.setTypeFilter(DownloadViewModel.Type.command.toString()) @@ -508,71 +495,68 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{ } - private fun updateWebsiteChips(list : List) { - websiteList = mutableListOf() - for (item in list){ - if (item.website == "null" || item.website.isEmpty()) continue - if (!websiteList.any { it.contentEquals(item.website, true) }) websiteList.add(item.website) - } - } override fun onCardClick(itemID: Long, isPresent: Boolean) { - val item = findItem(itemID) - UiUtil.showHistoryItemDetailsCard(item, requireActivity(), isPresent, - removeItem = { it, deleteFile -> - historyViewModel.delete(it, deleteFile) - }, - redownloadItem = { - val downloadItem = downloadViewModel.createDownloadItemFromHistory(it) - runBlocking{ - downloadViewModel.queueDownloads(listOf(downloadItem)) - } - historyViewModel.delete(it, false) - }, - redownloadShowDownloadCard = { - findNavController().navigate(R.id.downloadBottomSheetDialog, bundleOf( - Pair("result", downloadViewModel.createResultItemFromHistory(it)), - Pair("type", it.type), - )) + lifecycleScope.launch { + val item = withContext(Dispatchers.IO){ + historyViewModel.getByID(itemID) } - ) + + UiUtil.showHistoryItemDetailsCard(item, requireActivity(), isPresent, + removeItem = { it, deleteFile -> + historyViewModel.delete(it, deleteFile) + }, + redownloadItem = { + val downloadItem = downloadViewModel.createDownloadItemFromHistory(it) + runBlocking{ + downloadViewModel.queueDownloads(listOf(downloadItem)) + } + historyViewModel.delete(it, false) + }, + redownloadShowDownloadCard = { + findNavController().navigate(R.id.downloadBottomSheetDialog, bundleOf( + Pair("result", downloadViewModel.createResultItemFromHistory(it)), + Pair("type", it.type), + )) + } + ) + } } override fun onCardSelect(itemID: Long, isChecked: Boolean) { lifecycleScope.launch { - val item = findItem(itemID) - if (actionMode == null) actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar) - actionMode?.apply { - if (isChecked) selectedObjects.add(item!!) - else selectedObjects.remove(item!!) + val item = withContext(Dispatchers.IO) { + historyViewModel.getByID(itemID) + } - if (selectedObjects.size == 0){ + if (actionMode == null) actionMode = (activity as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar) + val selectedObjects = historyAdapter.getSelectedObjectsCount(totalCount) + actionMode?.apply { + if (selectedObjects == 0){ this.finish() }else{ - actionMode?.title = "${selectedObjects.size} ${getString(R.string.selected)}" + actionMode?.title = "$selectedObjects ${getString(R.string.selected)}" this.menu.findItem(R.id.select_between).isVisible = false - if(selectedObjects.size == 2){ - val selectedIDs = selectedObjects.sortedBy { it.id } - val resultsInMiddle = withContext(Dispatchers.IO){ - historyViewModel.getRecordsBetweenTwoItems(selectedIDs.first().id, selectedIDs.last().id) - }.toMutableList() - this.menu.findItem(R.id.select_between).isVisible = resultsInMiddle.isNotEmpty() + if(selectedObjects == 2){ + //TODO +// val selectedIDs = contextualActionBar.getSelectedIDs().sortedBy { it } +// val resultsInMiddle = withContext(Dispatchers.IO){ +// historyViewModel.getRecordsBetweenTwoItems(selectedIDs.first().id, selectedIDs.last().id) +// }.toMutableList() +// this.menu.findItem(R.id.select_between).isVisible = resultsInMiddle.isNotEmpty() } } } } } - private fun findItem(id : Long): HistoryItem? { - return historyList?.find { it?.id == id } - } private val contextualActionBar = object : ActionMode.Callback { override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { mode!!.menuInflater.inflate(R.menu.history_menu_context, menu) - mode.title = "${selectedObjects.size} ${getString(R.string.selected)}" + mode.title = "${historyAdapter.getSelectedObjectsCount(totalCount)} ${getString(R.string.selected)}" (activity as MainActivity).disableBottomNavigation() - topAppBar!!.menu.forEach { it.isEnabled = false } + topAppBar.menu.forEach { it.isEnabled = false } return true } @@ -589,62 +573,66 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{ ): Boolean { return when (item!!.itemId) { R.id.select_between -> { - lifecycleScope.launch { - val selectedIDs = selectedObjects.sortedBy { it.id } - val resultsInMiddle = withContext(Dispatchers.IO){ - historyViewModel.getRecordsBetweenTwoItems(selectedIDs.first().id, selectedIDs.last().id) - }.toMutableList() - if (resultsInMiddle.isNotEmpty()){ - selectedObjects.addAll(resultsInMiddle) - historyAdapter?.checkMultipleItems(selectedObjects.map { it.id }) - actionMode?.title = "${selectedObjects.count()} ${getString(R.string.selected)}" - } - mode?.menu?.findItem(R.id.select_between)?.isVisible = false - } + //TODO +// lifecycleScope.launch { +// val selectedIDs = selectedObjects.sortedBy { it.id } +// val resultsInMiddle = withContext(Dispatchers.IO){ +// historyViewModel.getRecordsBetweenTwoItems(selectedIDs.first().id, selectedIDs.last().id) +// }.toMutableList() +// if (resultsInMiddle.isNotEmpty()){ +// selectedObjects.addAll(resultsInMiddle) +// historyAdapter.checkMultipleItems(selectedObjects.map { it.id }) +// actionMode?.title = "${selectedObjects.count()} ${getString(R.string.selected)}" +// } +// mode?.menu?.findItem(R.id.select_between)?.isVisible = false +// } true } R.id.delete_results -> { - val deleteFile = booleanArrayOf(false) - val deleteDialog = MaterialAlertDialogBuilder(fragmentContext!!) - deleteDialog.setTitle(getString(R.string.you_are_going_to_delete_multiple_items)) - deleteDialog.setMultiChoiceItems( - arrayOf(getString(R.string.delete_files_too)), - booleanArrayOf(false) - ) { _: DialogInterface?, _: Int, b: Boolean -> deleteFile[0] = b } - deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() } - deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int -> - for (obj in selectedObjects){ - historyViewModel.delete(obj, deleteFile[0]) - } - clearCheckedItems() - actionMode?.finish() - } - deleteDialog.show() + //TODO +// val deleteFile = booleanArrayOf(false) +// val deleteDialog = MaterialAlertDialogBuilder(fragmentContext!!) +// deleteDialog.setTitle(getString(R.string.you_are_going_to_delete_multiple_items)) +// deleteDialog.setMultiChoiceItems( +// arrayOf(getString(R.string.delete_files_too)), +// booleanArrayOf(false) +// ) { _: DialogInterface?, _: Int, b: Boolean -> deleteFile[0] = b } +// deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() } +// deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int -> +// for (obj in selectedObjects){ +// historyViewModel.delete(obj, deleteFile[0]) +// } +// clearCheckedItems() +// actionMode?.finish() +// } +// deleteDialog.show() true } R.id.share -> { - FileUtil.shareFileIntent(requireContext(), selectedObjects.map { it.downloadPath }.flatten()) - clearCheckedItems() - actionMode?.finish() + //TODO +// FileUtil.shareFileIntent(requireContext(), selectedObjects.map { it.downloadPath }.flatten()) +// clearCheckedItems() +// actionMode?.finish() true } R.id.select_all -> { - historyAdapter?.checkAll(historyList) - selectedObjects.clear() - historyList?.forEach { selectedObjects.add(it!!) } - mode?.title = "(${selectedObjects.size}) ${resources.getString(R.string.all_items_selected)}" + //TODO +// historyAdapter.checkAll() +// historyList?.forEach { selectedObjects.add(it!!) } +// mode?.title = "(${selectedObjects.size}) ${resources.getString(R.string.all_items_selected)}" true } R.id.invert_selected -> { - historyAdapter?.invertSelected(historyList) - val invertedList = arrayListOf() - historyList?.forEach { - if (!selectedObjects.contains(it)!!) invertedList.add(it!!) - } - selectedObjects.clear() - selectedObjects.addAll(invertedList) - actionMode!!.title = "${selectedObjects.size} ${getString(R.string.selected)}" - if (invertedList.isEmpty()) actionMode?.finish() + //TODO +// historyAdapter.invertSelected() +// val invertedList = arrayListOf() +// historyList?.forEach { +// if (!selectedObjects.contains(it)!!) invertedList.add(it!!) +// } +// selectedObjects.clear() +// selectedObjects.addAll(invertedList) +// actionMode!!.title = "${selectedObjects.size} ${getString(R.string.selected)}" +// if (invertedList.isEmpty()) actionMode?.finish() true } else -> false @@ -654,15 +642,23 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{ override fun onDestroyActionMode(mode: ActionMode?) { actionMode = null (activity as MainActivity).enableBottomNavigation() - clearCheckedItems() - topAppBar!!.menu.forEach { it.isEnabled = true } + historyAdapter.clearCheckedItems() + topAppBar.menu.forEach { it.isEnabled = true } + } + + suspend fun getSelectedIDs() : List{ + //TODO +// return if (historyAdapter.inverted || historyAdapter.checkedItems.isEmpty()) { +// withContext(Dispatchers.IO){ +// historyViewModel.getItemIDsNotPresentIn(historyAdapter.checkedItems.toList()) +// } +// }else{ +// historyAdapter.checkedItems.toList() +// } + return listOf() } } - private fun clearCheckedItems(){ - historyAdapter?.clearCheckeditems() - selectedObjects.clear() - } private var simpleCallback: ItemTouchHelper.SimpleCallback = object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) { @@ -672,34 +668,44 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{ } override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + val itemID = viewHolder.itemView.tag.toString().toLong() val position = viewHolder.bindingAdapterPosition when (direction) { ItemTouchHelper.LEFT -> { - val deletedItem = historyList!![position] - historyAdapter!!.notifyItemChanged(position) - UiUtil.showRemoveHistoryItemDialog(deletedItem!!, requireActivity(), - delete = { item, deleteFile -> - lifecycleScope.launch { - withContext(Dispatchers.IO){ - historyViewModel.delete(item, deleteFile) - } + lifecycleScope.launch { + val deletedItem = withContext(Dispatchers.IO) { + historyViewModel.getByID(itemID) + } + historyAdapter.notifyItemChanged(position) + UiUtil.showRemoveHistoryItemDialog(deletedItem, requireActivity(), + delete = { item, deleteFile -> + lifecycleScope.launch { + withContext(Dispatchers.IO){ + historyViewModel.delete(item, deleteFile) + } - if (!deleteFile){ - Snackbar.make(recyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_INDEFINITE) - .setAction(getString(R.string.undo)) { - historyViewModel.insert(deletedItem) - }.show() + if (!deleteFile){ + Snackbar.make(recyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_INDEFINITE) + .setAction(getString(R.string.undo)) { + historyViewModel.insert(deletedItem) + }.show() + } } } - }) + ) + } } ItemTouchHelper.RIGHT -> { - val item = historyList!![position]!! - historyAdapter!!.notifyItemChanged(position) - findNavController().navigate(R.id.downloadBottomSheetDialog, bundleOf( - Pair("result", downloadViewModel.createResultItemFromHistory(item)), - Pair("type", item.type) - )) + lifecycleScope.launch { + val item = withContext(Dispatchers.IO) { + historyViewModel.getByID(itemID) + } + historyAdapter.notifyItemChanged(position) + findNavController().navigate(R.id.downloadBottomSheetDialog, bundleOf( + Pair("result", downloadViewModel.createResultItemFromHistory(item)), + Pair("type", item.type) + )) + } } } } @@ -737,7 +743,7 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{ super.onChildDraw( c, recyclerView, - viewHolder!!, + viewHolder, dX, dY, actionState, @@ -748,7 +754,13 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{ override fun onButtonClick(itemID: Long, isPresent: Boolean) { if (isPresent){ - FileUtil.shareFileIntent(requireContext(), historyList!!.first { it!!.id == itemID }!!.downloadPath) + lifecycleScope.launch { + val item = withContext(Dispatchers.IO){ + historyViewModel.getByID(itemID) + } + FileUtil.shareFileIntent(requireContext(), item.downloadPath) + } + } } companion object { diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/UpdateSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/UpdateSettingsFragment.kt index 3f160612..24e6531d 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/UpdateSettingsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/UpdateSettingsFragment.kt @@ -49,6 +49,12 @@ class UpdateSettingsFragment : BaseSettingsFragment() { true } + + ytdlVersion!!.setOnPreferenceClickListener { + initYTDLUpdate(editor) + true + } + updateYTDL!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { initYTDLUpdate(editor) diff --git a/app/src/main/java/com/deniscerri/ytdl/util/Extensions.kt b/app/src/main/java/com/deniscerri/ytdl/util/Extensions.kt index dd948fa9..917c0c30 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/Extensions.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/Extensions.kt @@ -46,9 +46,6 @@ import com.deniscerri.ytdl.database.models.Format import com.deniscerri.ytdl.database.models.observeSources.ObserveSourcesItem import com.deniscerri.ytdl.database.repository.DownloadRepository import com.deniscerri.ytdl.database.repository.ObserveSourcesRepository.EveryCategory -import com.deniscerri.ytdl.util.Extensions.isYoutubeChannelURL -import com.deniscerri.ytdl.util.Extensions.isYoutubeURL -import com.deniscerri.ytdl.util.Extensions.toTimePeriodsArray import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.tabs.TabLayout @@ -59,6 +56,7 @@ import com.neo.highlight.util.listener.HighlightTextWatcher import com.neo.highlight.util.scheme.ColorScheme import com.squareup.picasso.Picasso import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch import kotlinx.serialization.json.Json @@ -526,11 +524,43 @@ object Extensions { } fun String.extractURL() : String { - val res = Pattern.compile("(http|ftp|https)://([\\w_-]+(?:\\.[\\w_-]+)+)([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])").matcher(this) - return if (res.find()){ + val res = + Pattern.compile("(http|ftp|https)://([\\w_-]+(?:\\.[\\w_-]+)+)([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])") + .matcher(this) + return if (res.find()) { res.group() - }else{ + } else { this } } + + fun String.isURL(): Boolean { + return Pattern.compile("(http|ftp|https)://([\\w_-]+(?:\\.[\\w_-]+)+)([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])").matcher(this).find() + } + + fun combine( + flow: Flow, + flow2: Flow, + flow3: Flow, + flow4: Flow, + flow5: Flow, + flow6: Flow, + flow7: Flow, + transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R + ): Flow = combine( + flow, + combine(flow2, flow3, ::Pair), + combine(flow4, flow5, ::Pair), + combine(flow6, flow7, ::Pair), + ) { t1, t2, t3, t4, -> + transform( + t1, + t2.first, + t2.second, + t3.first, + t3.second, + t4.first, + t4.second + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt index 9ab80a72..31f89a46 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt @@ -191,6 +191,7 @@ object UiUtil { val extraCommandsSwitch : MaterialSwitch = bottomSheet.findViewById(R.id.extraCommandsSwitch)!! val extraCommandsAudio : CheckBox = bottomSheet.findViewById(R.id.checkbox_audio)!! val extraCommandsVideo : CheckBox = bottomSheet.findViewById(R.id.checkbox_video)!! + val extraCommandsDataFetchingSwitch : MaterialSwitch = bottomSheet.findViewById(R.id.extraCommandDataFetching)!! val shortcutsChipGroup : ChipGroup = bottomSheet.findViewById(R.id.shortcutsChipGroup)!! val editShortcuts : Button = bottomSheet.findViewById(R.id.edit_shortcuts)!! @@ -252,6 +253,7 @@ object UiUtil { if (item != null){ preferredCommandSwitch.isChecked = item.content == sharedPreferences.getString("preferred_command_template", "") + extraCommandsDataFetchingSwitch.isChecked = item.useAsExtraCommandDataFetching extraCommandsSwitch.isChecked = item.useAsExtraCommand if (item.useAsExtraCommand){ @@ -307,7 +309,7 @@ object UiUtil { if (item == null){ - val t = CommandTemplate(0, title.editText!!.text.toString(), content.editText!!.text.toString(), extraCommandsSwitch.isChecked, extraCommandsAudio.isChecked, extraCommandsVideo.isChecked) + val t = CommandTemplate(0, title.editText!!.text.toString(), content.editText!!.text.toString(), extraCommandsSwitch.isChecked, extraCommandsAudio.isChecked, extraCommandsVideo.isChecked, extraCommandsDataFetchingSwitch.isChecked) commandTemplateViewModel.insert(t) newTemplate(t) }else{ @@ -316,6 +318,7 @@ object UiUtil { item.useAsExtraCommand = extraCommandsSwitch.isChecked item.useAsExtraCommandAudio = extraCommandsAudio.isChecked item.useAsExtraCommandVideo = extraCommandsVideo.isChecked + item.useAsExtraCommandDataFetching = extraCommandsDataFetchingSwitch.isChecked commandTemplateViewModel.update(item) newTemplate(item) } diff --git a/app/src/main/java/com/deniscerri/ytdl/work/DownloadWorker.kt b/app/src/main/java/com/deniscerri/ytdl/work/DownloadWorker.kt index 9dcc5d5a..3e3b7310 100644 --- a/app/src/main/java/com/deniscerri/ytdl/work/DownloadWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdl/work/DownloadWorker.kt @@ -113,8 +113,8 @@ class DownloadWorker( setForegroundAsync(ForegroundInfo(notificationID, workNotif)) } - queuedItems.collectLatest { items -> - if (this@DownloadWorker.isStopped) return@collectLatest + queuedItems.collect { items -> + if (this@DownloadWorker.isStopped) return@collect runningYTDLInstances.clear() val activeDownloads = dao.getActiveDownloadsList() @@ -126,19 +126,19 @@ class DownloadWorker( val useScheduler = sharedPreferences.getBoolean("use_scheduler", false) if (items.isEmpty() && running.isEmpty()) { WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id) - return@collectLatest + return@collect } if (useScheduler){ if (items.none{it.downloadStartTime > 0L} && running.isEmpty() && !alarmScheduler.isDuringTheScheduledTime()) { WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id) - return@collectLatest + return@collect } } if (priorityItemIDs.isEmpty() && !continueAfterPriorityIds) { WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id) - return@collectLatest + return@collect } val concurrentDownloads = sharedPreferences.getInt("concurrent_downloads", 1) - running.size @@ -312,6 +312,7 @@ class DownloadWorker( finalPaths, downloadItem.website, downloadItem.format, + downloadItem.format.filesize, downloadItem.id, commandString) historyDao.insert(historyItem) diff --git a/app/src/main/res/layout/create_command_template.xml b/app/src/main/res/layout/create_command_template.xml index c96001e1..4c16e043 100644 --- a/app/src/main/res/layout/create_command_template.xml +++ b/app/src/main/res/layout/create_command_template.xml @@ -157,6 +157,20 @@ + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 70423919..7b91718b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -434,6 +434,6 @@ Socket Timeout Time to wait before giving up, in seconds Advanced - Data Fetching Extra Commands + Data Fetching Extra Command Thumbnail \ No newline at end of file diff --git a/app/src/main/res/xml/advanced_preferences.xml b/app/src/main/res/xml/advanced_preferences.xml index e2a4f711..85b314a4 100644 --- a/app/src/main/res/xml/advanced_preferences.xml +++ b/app/src/main/res/xml/advanced_preferences.xml @@ -2,32 +2,27 @@ - + + - + - - - + + \ No newline at end of file diff --git a/app/src/main/res/xml/updating_preferences.xml b/app/src/main/res/xml/updating_preferences.xml index b55541bb..18c512eb 100644 --- a/app/src/main/res/xml/updating_preferences.xml +++ b/app/src/main/res/xml/updating_preferences.xml @@ -2,6 +2,11 @@ + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - \ No newline at end of file