history revamp paginated wip

This commit is contained in:
zaednasr 2024-11-13 21:57:07 +01:00
parent 0c00ab350d
commit 8ae2a4d8a3
No known key found for this signature in database
GPG key ID: 92B1DE23AD3D0E9E
24 changed files with 2249 additions and 322 deletions

View file

@ -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')"
]
}
}

View file

@ -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')"
]
}
}

View file

@ -52,8 +52,6 @@ import com.deniscerri.ytdl.ui.downloads.DownloadQueueMainFragment
import com.deniscerri.ytdl.ui.downloads.HistoryFragment import com.deniscerri.ytdl.ui.downloads.HistoryFragment
import com.deniscerri.ytdl.ui.more.settings.SettingsActivity import com.deniscerri.ytdl.ui.more.settings.SettingsActivity
import com.deniscerri.ytdl.util.CrashListener 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
import com.deniscerri.ytdl.util.NavbarUtil.applyNavBarStyle import com.deniscerri.ytdl.util.NavbarUtil.applyNavBarStyle
import com.deniscerri.ytdl.util.ThemeUtil import com.deniscerri.ytdl.util.ThemeUtil
@ -440,7 +438,7 @@ class MainActivity : BaseActivity() {
textBuilder.append(c.toChar()) textBuilder.append(c.toChar())
} }
val bundle = Bundle() val bundle = Bundle()
bundle.putString("url", textBuilder.toString().extractURL()) bundle.putString("url", textBuilder.toString())
navController.popBackStack(R.id.homeFragment, true) navController.popBackStack(R.id.homeFragment, true)
navController.navigate( navController.navigate(
R.id.homeFragment, R.id.homeFragment,

View file

@ -41,7 +41,7 @@ import com.deniscerri.ytdl.database.models.TerminalItem
TerminalItem::class, TerminalItem::class,
ObserveSourcesItem::class ObserveSourcesItem::class
], ],
version = 19, version = 21,
autoMigrations = [ autoMigrations = [
AutoMigration (from = 1, to = 2), AutoMigration (from = 1, to = 2),
AutoMigration (from = 2, to = 3), AutoMigration (from = 2, to = 3),
@ -61,6 +61,8 @@ import com.deniscerri.ytdl.database.models.TerminalItem
AutoMigration (from = 16, to = 17), AutoMigration (from = 16, to = 17),
AutoMigration (from = 17, to = 18), AutoMigration (from = 17, to = 18),
AutoMigration (from = 18, to = 19), AutoMigration (from = 18, to = 19),
AutoMigration (from = 19, to = 20),
//AutoMigration (from = 20, to = 21) MANUALLY HANDLED
] ]
) )
abstract class DBManager : RoomDatabase(){ abstract class DBManager : RoomDatabase(){

View file

@ -5,6 +5,8 @@ import androidx.room.DeleteTable
import androidx.room.migration.AutoMigrationSpec import androidx.room.migration.AutoMigrationSpec
import androidx.room.migration.Migration import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import com.deniscerri.ytdl.database.models.Format
import com.google.gson.Gson
object Migrations { object Migrations {
@ -28,6 +30,19 @@ object Migrations {
// Migration(17, 18 ){ database -> // Migration(17, 18 ){ database ->
// database.execSQL("ALTER TABLE `sources` ADD COLUMN `syncWithSource` INTEGER NOT NULL DEFAULT 0") // 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( @DeleteTable.Entries(

View file

@ -1,10 +1,12 @@
package com.deniscerri.ytdl.database.dao package com.deniscerri.ytdl.database.dao
import androidx.paging.PagingSource
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import androidx.room.Update import androidx.room.Update
import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.models.HistoryItem import com.deniscerri.ytdl.database.models.HistoryItem
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -30,9 +32,44 @@ interface HistoryDao {
fun getHistorySortedByAuthor(query : String, type : String, site : String, sort : String) : List<HistoryItem> fun getHistorySortedByAuthor(query : String, type : String, site : String, sort : String) : List<HistoryItem>
@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<Int, HistoryItem>
@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<Int, HistoryItem>
@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<Int, HistoryItem>
@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<Int, HistoryItem>
@Query("SELECT * FROM history") @Query("SELECT * FROM history")
fun getAllHistory() : Flow<List<HistoryItem>> fun getAllHistory() : Flow<List<HistoryItem>>
@Query("SELECT DISTINCT website FROM history")
fun getWebsites() : Flow<List<String>>
@Query("SELECT COUNT(*) FROM history")
fun getCount() : Flow<Int>
@Query("SELECT * FROM history")
fun getAllHistoryPaginated() : PagingSource<Int, HistoryItem>
@Query("SELECT * FROM history") @Query("SELECT * FROM history")
fun getAllHistoryList() : List<HistoryItem> fun getAllHistoryList() : List<HistoryItem>

View file

@ -17,5 +17,7 @@ data class CommandTemplate(
@ColumnInfo(defaultValue = "1") @ColumnInfo(defaultValue = "1")
var useAsExtraCommandAudio: Boolean, var useAsExtraCommandAudio: Boolean,
@ColumnInfo(defaultValue = "1") @ColumnInfo(defaultValue = "1")
var useAsExtraCommandVideo: Boolean var useAsExtraCommandVideo: Boolean,
@ColumnInfo(defaultValue = "0")
var useAsExtraCommandDataFetching: Boolean
) )

View file

@ -20,6 +20,8 @@ data class HistoryItem(
val website: String, val website: String,
val format: Format, val format: Format,
@ColumnInfo(defaultValue = "0") @ColumnInfo(defaultValue = "0")
val filesize: Long = 0,
@ColumnInfo(defaultValue = "0")
val downloadId: Long, val downloadId: Long,
@ColumnInfo(defaultValue = "") @ColumnInfo(defaultValue = "")
val command: String = "" val command: String = ""

View file

@ -1,5 +1,10 @@
package com.deniscerri.ytdl.database.repository 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.DBManager.SORTING
import com.deniscerri.ytdl.database.dao.HistoryDao import com.deniscerri.ytdl.database.dao.HistoryDao
import com.deniscerri.ytdl.database.models.HistoryItem 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 com.deniscerri.ytdl.util.FileUtil
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
class HistoryRepository(private val historyDao: HistoryDao) { class HistoryRepository(private val historyDao: HistoryDao) {
val items : Flow<List<HistoryItem>> = historyDao.getAllHistory() val items : Flow<List<HistoryItem>> = historyDao.getAllHistory()
val websites : Flow<List<String>> = historyDao.getWebsites()
val count : Flow<Int> = historyDao.getCount()
enum class HistorySortType { enum class HistorySortType {
DATE, TITLE, AUTHOR, FILESIZE DATE, TITLE, AUTHOR, FILESIZE
@ -27,6 +35,7 @@ class HistoryRepository(private val historyDao: HistoryDao) {
return historyDao.getAllHistoryByURL(url) return historyDao.getAllHistoryByURL(url)
} }
fun getFiltered(query : String, type : String, site : String, sortType: HistorySortType, sort: SORTING, statusFilter: HistoryViewModel.HistoryStatus) : List<HistoryItem> { fun getFiltered(query : String, type : String, site : String, sortType: HistorySortType, sort: SORTING, statusFilter: HistoryViewModel.HistoryStatus) : List<HistoryItem> {
var filtered = when(sortType){ var filtered = when(sortType){
HistorySortType.DATE -> historyDao.getHistorySortedByID(query, type, site, sort.toString()) HistorySortType.DATE -> historyDao.getHistorySortedByID(query, type, site, sort.toString())
@ -54,6 +63,20 @@ class HistoryRepository(private val historyDao: HistoryDao) {
return filtered return filtered
} }
fun getPaginatedSource(query : String, type : String, site : String, sortType: HistorySortType, sort: SORTING) : PagingSource<Int, HistoryItem> {
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){ suspend fun insert(item: HistoryItem){
historyDao.insert(item) historyDao.insert(item)
} }

View file

@ -550,7 +550,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} }
return generateCommandFormat( return generateCommandFormat(
c ?: CommandTemplate(0, "", preferredTemplateContent, useAsExtraCommand = false, useAsExtraCommandAudio = false, useAsExtraCommandVideo = false) c ?: CommandTemplate(0, "", preferredTemplateContent, useAsExtraCommand = false, useAsExtraCommandAudio = false, useAsExtraCommandVideo = false, useAsExtraCommandDataFetching = false)
) )
} }
} }

View file

@ -6,68 +6,109 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.lifecycle.switchMap
import androidx.lifecycle.viewModelScope 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
import com.deniscerri.ytdl.database.DBManager.SORTING import com.deniscerri.ytdl.database.DBManager.SORTING
import com.deniscerri.ytdl.database.models.HistoryItem 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
import com.deniscerri.ytdl.database.repository.HistoryRepository.HistorySortType import com.deniscerri.ytdl.database.repository.HistoryRepository.HistorySortType
import com.deniscerri.ytdl.util.Extensions import com.deniscerri.ytdl.util.Extensions
import com.deniscerri.ytdl.util.FileUtil
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.cache
import kotlinx.coroutines.flow.combine 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.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalCoroutinesApi::class)
class HistoryViewModel(application: Application) : AndroidViewModel(application) { class HistoryViewModel(application: Application) : AndroidViewModel(application) {
private val repository : HistoryRepository private val repository : HistoryRepository
val sortOrder = MutableLiveData(SORTING.DESC) val sortOrder = MutableStateFlow(SORTING.DESC)
val sortType = MutableLiveData(HistorySortType.DATE) val sortType = MutableStateFlow(HistorySortType.DATE)
val websiteFilter = MutableLiveData("") val websiteFilter = MutableStateFlow("")
val statusFilter = MutableLiveData(HistoryStatus.ALL) val statusFilter = MutableStateFlow(HistoryStatus.ALL)
private val queryFilter = MutableLiveData("") private val queryFilter = MutableStateFlow("")
private val typeFilter = MutableLiveData("") private val typeFilter = MutableStateFlow("")
enum class HistoryStatus { enum class HistoryStatus {
UNSET, DELETED, NOT_DELETED, ALL UNSET, DELETED, NOT_DELETED, ALL
} }
val allItems : LiveData<List<HistoryItem>>
private var _items = MediatorLiveData<List<HistoryItem>>() private var _items = MediatorLiveData<List<HistoryItem>>()
var paginatedItems : Flow<PagingData<HistoryItem>>
var websites : Flow<List<String>>
var totalCount : Flow<Int>
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 { init {
val dao = DBManager.getInstance(application).historyDao val dao = DBManager.getInstance(application).historyDao
repository = HistoryRepository(dao) repository = HistoryRepository(dao)
allItems = repository.items.asLiveData() websites = repository.websites
_items.addSource(allItems){ totalCount = repository.count
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!!)
}
_items.addSource(statusFilter){ val filters = listOf(dao.getAllHistory(), sortOrder, sortType, websiteFilter, statusFilter, queryFilter, typeFilter)
filter(queryFilter.value!!, typeFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, statusFilter.value!!) 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<List<HistoryItem>>{ when(status) {
return _items 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){ 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){ 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<HistoryItem> { fun getAll() : List<HistoryItem> {

View file

@ -41,8 +41,6 @@ import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.ui.BaseActivity import com.deniscerri.ytdl.ui.BaseActivity
import com.deniscerri.ytdl.util.Extensions.extractURL import com.deniscerri.ytdl.util.Extensions.extractURL
import com.deniscerri.ytdl.util.ThemeUtil 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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
@ -50,7 +48,6 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlin.properties.Delegates import kotlin.properties.Delegates
import kotlin.system.exitProcess
class ShareActivity : BaseActivity() { class ShareActivity : BaseActivity() {

View file

@ -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
import com.deniscerri.ytdl.ui.downloads.HistoryFragment.Companion import com.deniscerri.ytdl.ui.downloads.HistoryFragment.Companion
import com.deniscerri.ytdl.util.Extensions.enableFastScroll 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.NotificationUtil
import com.deniscerri.ytdl.util.ThemeUtil import com.deniscerri.ytdl.util.ThemeUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
@ -231,7 +232,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
val url = requireArguments().getString("url") val url = requireArguments().getString("url")
if (inputQueries == null) inputQueries = mutableListOf() if (inputQueries == null) inputQueries = mutableListOf()
searchBar?.setText(url) searchBar?.setText(url)
val argList = url!!.split("\n").toMutableList() val argList = url!!.split("\n").filter { it.isURL() }.toMutableList()
argList.removeAll(listOf("", null)) argList.removeAll(listOf("", null))
inputQueries!!.addAll(argList) inputQueries!!.addAll(argList)
} }

View file

@ -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<HistoryItem, HistoryPaginatedAdapter.ViewHolder>(
DIFF_CALLBACK
) {
val checkedItems: MutableSet<Long>
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<ImageView>(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<TextView>(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<TextView>(R.id.downloads_info_bottom)
author.text = item.author
val length = card.findViewById<TextView>(R.id.length)
length.text = if(item.downloadPath.size == 1) item.duration else ""
// TIME DOWNLOADED ----------------------------------
val datetime = card.findViewById<TextView>(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<FloatingActionButton>(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<Long>){
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<HistoryItem> = object : DiffUtil.ItemCallback<HistoryItem>() {
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
}
}
}
}

View file

@ -93,7 +93,7 @@ class DownloadCommandFragment(private val resultItem: ResultItem? = null, privat
preferences.edit().putString("lastCommandTemplateUsed", downloadItem.format.format_note).apply() preferences.edit().putString("lastCommandTemplateUsed", downloadItem.format.format_note).apply()
if (!Patterns.WEB_URL.matcher(downloadItem.url).matches() && downloadItem.url.endsWith(".txt")){ 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 = "" downloadItem.url = ""
} }
@ -249,7 +249,7 @@ class DownloadCommandFragment(private val resultItem: ResultItem? = null, privat
0, 0,
"", "",
chosenCommandView.editText!!.text.toString(), chosenCommandView.editText!!.text.toString(),
useAsExtraCommand = false, useAsExtraCommandAudio = false, useAsExtraCommandVideo = false useAsExtraCommand = false, useAsExtraCommandAudio = false, useAsExtraCommandVideo = false, useAsExtraCommandDataFetching = false
) )
UiUtil.showCommandTemplateCreationOrUpdatingSheet( UiUtil.showCommandTemplateCreationOrUpdatingSheet(
current, requireActivity(), viewLifecycleOwner, commandTemplateViewModel, current, requireActivity(), viewLifecycleOwner, commandTemplateViewModel,

View file

@ -1,7 +1,6 @@
package com.deniscerri.ytdl.ui.downloads package com.deniscerri.ytdl.ui.downloads
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.graphics.Canvas import android.graphics.Canvas
@ -16,8 +15,6 @@ import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup import android.view.ViewGroup
import android.view.Window import android.view.Window
import android.widget.LinearLayout import android.widget.LinearLayout
@ -45,13 +42,12 @@ import com.deniscerri.ytdl.MainActivity
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.DBManager.SORTING import com.deniscerri.ytdl.database.DBManager.SORTING
import com.deniscerri.ytdl.database.models.HistoryItem 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
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdl.ui.adapter.HistoryAdapter import com.deniscerri.ytdl.ui.adapter.HistoryPaginatedAdapter
import com.deniscerri.ytdl.ui.downloadcard.FormatSelectionBottomSheetDialog.FormatCategory
import com.deniscerri.ytdl.util.Extensions.enableFastScroll 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.FileUtil
import com.deniscerri.ytdl.util.NavbarUtil import com.deniscerri.ytdl.util.NavbarUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
@ -73,26 +69,23 @@ import kotlinx.coroutines.withContext
/** /**
* A fragment representing a list of Items. * 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 historyViewModel : HistoryViewModel
private lateinit var downloadViewModel : DownloadViewModel private lateinit var downloadViewModel : DownloadViewModel
private var fragmentView: View? = null private lateinit var fragmentView: View
private var activity: Activity? = null
private var mainActivity: MainActivity? = null private var mainActivity: MainActivity? = null
private var fragmentContext: Context? = null private var fragmentContext: Context? = null
private var layoutinflater: LayoutInflater? = null private lateinit var layoutinflater: LayoutInflater
private var topAppBar: MaterialToolbar? = null private lateinit var topAppBar: MaterialToolbar
private lateinit var recyclerView: RecyclerView private lateinit var recyclerView: RecyclerView
private var historyAdapter: HistoryAdapter? = null private lateinit var historyAdapter: HistoryPaginatedAdapter
private var sortSheet: BottomSheetDialog? = null private lateinit var sortSheet: BottomSheetDialog
private var uiHandler: Handler? = null private lateinit var uiHandler: Handler
private var noResults: RelativeLayout? = null private lateinit var noResults: RelativeLayout
private var selectionChips: LinearLayout? = null private lateinit var selectionChips: LinearLayout
private var historyList: List<HistoryItem?>? = null
private var allhistoryList: List<HistoryItem?>? = null
private var websiteList: MutableList<String> = mutableListOf() private var websiteList: MutableList<String> = mutableListOf()
private lateinit var selectedObjects: ArrayList<HistoryItem> private var totalCount = 0
private var actionMode : ActionMode? = null private var actionMode : ActionMode? = null
private lateinit var sortChip: Chip private lateinit var sortChip: Chip
@ -100,9 +93,8 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View {
fragmentView = inflater.inflate(R.layout.fragment_history, container, false) fragmentView = inflater.inflate(R.layout.fragment_history, container, false)
activity = getActivity()
mainActivity = activity as MainActivity? mainActivity = activity as MainActivity?
return fragmentView return fragmentView
} }
@ -118,24 +110,15 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
selectionChips = view.findViewById(R.id.history_selection_chips) selectionChips = view.findViewById(R.id.history_selection_chips)
uiHandler = Handler(Looper.getMainLooper()) uiHandler = Handler(Looper.getMainLooper())
sortChip = view.findViewById(R.id.sortChip) sortChip = view.findViewById(R.id.sortChip)
selectedObjects = arrayListOf()
val isInNavBar = NavbarUtil.getNavBarItems(requireActivity()).any { n -> n.itemId == R.id.historyFragment && n.isVisible } val isInNavBar = NavbarUtil.getNavBarItems(requireActivity()).any { n -> n.itemId == R.id.historyFragment && n.isVisible }
if (isInNavBar) { if (isInNavBar) {
topAppBar?.navigationIcon = null topAppBar.navigationIcon = null
}else{ }else{
mainActivity?.hideBottomNavigation() mainActivity?.hideBottomNavigation()
} }
topAppBar?.setNavigationOnClickListener { mainActivity?.onBackPressedDispatcher?.onBackPressed() } topAppBar.setNavigationOnClickListener { mainActivity?.onBackPressedDispatcher?.onBackPressed() }
historyAdapter = HistoryPaginatedAdapter(this, requireActivity())
historyList = mutableListOf()
historyAdapter =
HistoryAdapter(
this,
requireActivity()
)
recyclerView = view.findViewById(R.id.recyclerviewhistorys) recyclerView = view.findViewById(R.id.recyclerviewhistorys)
recyclerView.enableFastScroll() recyclerView.enableFastScroll()
@ -146,37 +129,44 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
} }
recyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size)) recyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size))
noResults?.visibility = GONE noResults.isVisible = false
historyViewModel = ViewModelProvider(this)[HistoryViewModel::class.java] historyViewModel = ViewModelProvider(this)[HistoryViewModel::class.java]
historyViewModel.allItems.observe(viewLifecycleOwner) { recyclerView.adapter = historyAdapter
allhistoryList = it lifecycleScope.launch {
if(it.isEmpty()){ historyViewModel.paginatedItems.collectLatest {
noResults!!.visibility = VISIBLE withContext(Dispatchers.IO){
selectionChips!!.visibility = GONE historyAdapter.submitData(it)
topAppBar!!.menu.children.firstOrNull { m -> m.itemId == R.id.filters }?.isVisible = false }
}else{ }
noResults!!.visibility = GONE }
selectionChips!!.visibility = VISIBLE lifecycleScope.launch {
topAppBar!!.menu.children.firstOrNull { m -> m.itemId == R.id.filters }?.isVisible = true historyViewModel.websites.collectLatest {
updateWebsiteChips(it) 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) { lifecycleScope.launch {
historyAdapter!!.submitList(it) historyViewModel.totalCount.collectLatest {
historyList = it totalCount = it
if (recyclerView.adapter == null){
recyclerView.adapter = historyAdapter
}else{
scrollToTop()
} }
} }
historyViewModel.sortOrder.observe(viewLifecycleOwner){ lifecycleScope.launch {
if (it != null){ historyViewModel.sortOrder.collectLatest {
when(it){ when(it){
SORTING.ASC -> sortChip.chipIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_down) SORTING.ASC -> sortChip.chipIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_down)
SORTING.DESC -> sortChip.chipIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_up) SORTING.DESC -> sortChip.chipIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_up)
@ -184,20 +174,18 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
} }
} }
historyViewModel.sortType.observe(viewLifecycleOwner){ lifecycleScope.launch {
if(it != null){ historyViewModel.sortType.collectLatest {
when(it){ when(it){
HistoryRepository.HistorySortType.AUTHOR -> sortChip.text = getString(R.string.author) HistoryRepository.HistorySortType.AUTHOR -> sortChip.text = getString(R.string.author)
HistoryRepository.HistorySortType.DATE -> sortChip.text = getString(R.string.date_added) HistoryRepository.HistorySortType.DATE -> sortChip.text = getString(R.string.date_added)
HistoryRepository.HistorySortType.TITLE -> sortChip.text = getString(R.string.title) HistoryRepository.HistorySortType.TITLE -> sortChip.text = getString(R.string.title)
HistoryRepository.HistorySortType.FILESIZE -> sortChip.text = getString(R.string.file_size) HistoryRepository.HistorySortType.FILESIZE -> sortChip.text = getString(R.string.file_size)
} }
} }
} }
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
lifecycleScope.launch{ lifecycleScope.launch{
downloadViewModel.alreadyExistsUiState.collectLatest { res -> downloadViewModel.alreadyExistsUiState.collectLatest { res ->
if (res.isNotEmpty()){ if (res.isNotEmpty()){
@ -212,7 +200,6 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
} }
} }
initMenu() initMenu()
initChips() initChips()
} }
@ -220,7 +207,7 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
fun scrollToTop() { fun scrollToTop() {
recyclerView.scrollToPosition(0) recyclerView.scrollToPosition(0)
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
(topAppBar!!.parent as AppBarLayout).setExpanded( (topAppBar.parent as AppBarLayout).setExpanded(
true, true,
true true
) )
@ -238,14 +225,14 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
return true return true
} }
} }
topAppBar!!.menu.findItem(R.id.search_history) topAppBar.menu.findItem(R.id.search_history)
.setOnActionExpandListener(onActionExpandListener) .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!!.inputType = InputType.TYPE_CLASS_TEXT
searchView.queryHint = getString(R.string.search_history_hint) searchView.queryHint = getString(R.string.search_history_hint)
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean { 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) historyViewModel.setQueryFilter(query)
return true return true
} }
@ -255,13 +242,13 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
return true return true
} }
}) })
topAppBar!!.setOnClickListener { scrollToTop() } topAppBar.setOnClickListener { scrollToTop() }
val showingDownloadQueue = NavbarUtil.getNavBarItems(requireContext()).any { n -> n.itemId == R.id.downloadQueueMainFragment && n.isVisible } val showingDownloadQueue = NavbarUtil.getNavBarItems(requireContext()).any { n -> n.itemId == R.id.downloadQueueMainFragment && n.isVisible }
topAppBar!!.menu.findItem(R.id.download_queue).isVisible = !showingDownloadQueue topAppBar.menu.findItem(R.id.download_queue).isVisible = !showingDownloadQueue
topAppBar!!.setOnMenuItemClickListener { m: MenuItem -> topAppBar.setOnMenuItemClickListener { m: MenuItem ->
when (m.itemId) { when (m.itemId) {
R.id.remove_history -> { R.id.remove_history -> {
if(allhistoryList!!.isEmpty()){ if(websiteList.isEmpty()){
Toast.makeText(context, R.string.history_is_empty, Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.history_is_empty, Toast.LENGTH_SHORT).show()
}else{ }else{
val deleteFile = booleanArrayOf(false) val deleteFile = booleanArrayOf(false)
@ -282,7 +269,7 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
findNavController().navigate(R.id.downloadQueueMainFragment) findNavController().navigate(R.id.downloadQueueMainFragment)
} }
R.id.remove_deleted_history -> { R.id.remove_deleted_history -> {
if(allhistoryList!!.isEmpty()){ if(websiteList.isEmpty()){
Toast.makeText(context, R.string.history_is_empty, Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.history_is_empty, Toast.LENGTH_SHORT).show()
}else{ }else{
val deleteDialog = MaterialAlertDialogBuilder(fragmentContext!!) val deleteDialog = MaterialAlertDialogBuilder(fragmentContext!!)
@ -296,7 +283,7 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
} }
} }
R.id.remove_duplicates -> { R.id.remove_duplicates -> {
if(allhistoryList!!.isEmpty()){ if(websiteList.isEmpty()){
Toast.makeText(context, R.string.history_is_empty, Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.history_is_empty, Toast.LENGTH_SHORT).show()
}else{ }else{
val deleteDialog = MaterialAlertDialogBuilder(fragmentContext!!) val deleteDialog = MaterialAlertDialogBuilder(fragmentContext!!)
@ -384,7 +371,7 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
for (i in websiteList.indices) { for (i in websiteList.indices) {
val w = websiteList[i] 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.text = w
tmp.id = i tmp.id = i
tmp.setOnClickListener { tmp.setOnClickListener {
@ -427,16 +414,16 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
private fun initChips() { private fun initChips() {
val sortChip = fragmentView!!.findViewById<Chip>(R.id.sortChip) val sortChip = fragmentView.findViewById<Chip>(R.id.sortChip)
sortChip.setOnClickListener { sortChip.setOnClickListener {
sortSheet = BottomSheetDialog(requireContext()) sortSheet = BottomSheetDialog(requireContext())
sortSheet!!.requestWindowFeature(Window.FEATURE_NO_TITLE) sortSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
sortSheet!!.setContentView(R.layout.history_sort_sheet) sortSheet.setContentView(R.layout.history_sort_sheet)
val date = sortSheet!!.findViewById<TextView>(R.id.date) val date = sortSheet.findViewById<TextView>(R.id.date)
val title = sortSheet!!.findViewById<TextView>(R.id.title) val title = sortSheet.findViewById<TextView>(R.id.title)
val author = sortSheet!!.findViewById<TextView>(R.id.author) val author = sortSheet.findViewById<TextView>(R.id.author)
val filesize = sortSheet!!.findViewById<TextView>(R.id.filesize) val filesize = sortSheet.findViewById<TextView>(R.id.filesize)
val sortOptions = listOf(date!!, title!!, author!!, filesize!!) val sortOptions = listOf(date!!, title!!, author!!, filesize!!)
sortOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) } sortOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) }
@ -469,12 +456,12 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
} }
val displayMetrics = DisplayMetrics() val displayMetrics = DisplayMetrics()
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics) requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
sortSheet!!.behavior.peekHeight = displayMetrics.heightPixels sortSheet.behavior.peekHeight = displayMetrics.heightPixels
sortSheet!!.show() sortSheet.show()
} }
//format //format
val audio = fragmentView!!.findViewById<Chip>(R.id.audio_chip) val audio = fragmentView.findViewById<Chip>(R.id.audio_chip)
audio.setOnClickListener { audio.setOnClickListener {
if (audio.isChecked) { if (audio.isChecked) {
historyViewModel.setTypeFilter(DownloadViewModel.Type.audio.toString()) historyViewModel.setTypeFilter(DownloadViewModel.Type.audio.toString())
@ -484,7 +471,7 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
audio.isChecked = false audio.isChecked = false
} }
} }
val video = fragmentView!!.findViewById<Chip>(R.id.video_chip) val video = fragmentView.findViewById<Chip>(R.id.video_chip)
video.setOnClickListener { video.setOnClickListener {
if (video.isChecked) { if (video.isChecked) {
historyViewModel.setTypeFilter(DownloadViewModel.Type.video.toString()) historyViewModel.setTypeFilter(DownloadViewModel.Type.video.toString())
@ -494,7 +481,7 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
video.isChecked = false video.isChecked = false
} }
} }
val command = fragmentView!!.findViewById<Chip>(R.id.command_chip) val command = fragmentView.findViewById<Chip>(R.id.command_chip)
command.setOnClickListener { command.setOnClickListener {
if (command.isChecked) { if (command.isChecked) {
historyViewModel.setTypeFilter(DownloadViewModel.Type.command.toString()) historyViewModel.setTypeFilter(DownloadViewModel.Type.command.toString())
@ -508,71 +495,68 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
} }
private fun updateWebsiteChips(list : List<HistoryItem>) {
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) { override fun onCardClick(itemID: Long, isPresent: Boolean) {
val item = findItem(itemID) lifecycleScope.launch {
UiUtil.showHistoryItemDetailsCard(item, requireActivity(), isPresent, val item = withContext(Dispatchers.IO){
removeItem = { it, deleteFile -> historyViewModel.getByID(itemID)
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),
))
} }
)
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) { override fun onCardSelect(itemID: Long, isChecked: Boolean) {
lifecycleScope.launch { lifecycleScope.launch {
val item = findItem(itemID) val item = withContext(Dispatchers.IO) {
if (actionMode == null) actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar) historyViewModel.getByID(itemID)
actionMode?.apply { }
if (isChecked) selectedObjects.add(item!!)
else selectedObjects.remove(item!!)
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() this.finish()
}else{ }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 this.menu.findItem(R.id.select_between).isVisible = false
if(selectedObjects.size == 2){ if(selectedObjects == 2){
val selectedIDs = selectedObjects.sortedBy { it.id } //TODO
val resultsInMiddle = withContext(Dispatchers.IO){ // val selectedIDs = contextualActionBar.getSelectedIDs().sortedBy { it }
historyViewModel.getRecordsBetweenTwoItems(selectedIDs.first().id, selectedIDs.last().id) // val resultsInMiddle = withContext(Dispatchers.IO){
}.toMutableList() // historyViewModel.getRecordsBetweenTwoItems(selectedIDs.first().id, selectedIDs.last().id)
this.menu.findItem(R.id.select_between).isVisible = resultsInMiddle.isNotEmpty() // }.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 { private val contextualActionBar = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
mode!!.menuInflater.inflate(R.menu.history_menu_context, menu) 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() (activity as MainActivity).disableBottomNavigation()
topAppBar!!.menu.forEach { it.isEnabled = false } topAppBar.menu.forEach { it.isEnabled = false }
return true return true
} }
@ -589,62 +573,66 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
): Boolean { ): Boolean {
return when (item!!.itemId) { return when (item!!.itemId) {
R.id.select_between -> { R.id.select_between -> {
lifecycleScope.launch { //TODO
val selectedIDs = selectedObjects.sortedBy { it.id } // lifecycleScope.launch {
val resultsInMiddle = withContext(Dispatchers.IO){ // val selectedIDs = selectedObjects.sortedBy { it.id }
historyViewModel.getRecordsBetweenTwoItems(selectedIDs.first().id, selectedIDs.last().id) // val resultsInMiddle = withContext(Dispatchers.IO){
}.toMutableList() // historyViewModel.getRecordsBetweenTwoItems(selectedIDs.first().id, selectedIDs.last().id)
if (resultsInMiddle.isNotEmpty()){ // }.toMutableList()
selectedObjects.addAll(resultsInMiddle) // if (resultsInMiddle.isNotEmpty()){
historyAdapter?.checkMultipleItems(selectedObjects.map { it.id }) // selectedObjects.addAll(resultsInMiddle)
actionMode?.title = "${selectedObjects.count()} ${getString(R.string.selected)}" // historyAdapter.checkMultipleItems(selectedObjects.map { it.id })
} // actionMode?.title = "${selectedObjects.count()} ${getString(R.string.selected)}"
mode?.menu?.findItem(R.id.select_between)?.isVisible = false // }
} // mode?.menu?.findItem(R.id.select_between)?.isVisible = false
// }
true true
} }
R.id.delete_results -> { R.id.delete_results -> {
val deleteFile = booleanArrayOf(false) //TODO
val deleteDialog = MaterialAlertDialogBuilder(fragmentContext!!) // val deleteFile = booleanArrayOf(false)
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete_multiple_items)) // val deleteDialog = MaterialAlertDialogBuilder(fragmentContext!!)
deleteDialog.setMultiChoiceItems( // deleteDialog.setTitle(getString(R.string.you_are_going_to_delete_multiple_items))
arrayOf(getString(R.string.delete_files_too)), // deleteDialog.setMultiChoiceItems(
booleanArrayOf(false) // arrayOf(getString(R.string.delete_files_too)),
) { _: DialogInterface?, _: Int, b: Boolean -> deleteFile[0] = b } // booleanArrayOf(false)
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() } // ) { _: DialogInterface?, _: Int, b: Boolean -> deleteFile[0] = b }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int -> // deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
for (obj in selectedObjects){ // deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
historyViewModel.delete(obj, deleteFile[0]) // for (obj in selectedObjects){
} // historyViewModel.delete(obj, deleteFile[0])
clearCheckedItems() // }
actionMode?.finish() // clearCheckedItems()
} // actionMode?.finish()
deleteDialog.show() // }
// deleteDialog.show()
true true
} }
R.id.share -> { R.id.share -> {
FileUtil.shareFileIntent(requireContext(), selectedObjects.map { it.downloadPath }.flatten()) //TODO
clearCheckedItems() // FileUtil.shareFileIntent(requireContext(), selectedObjects.map { it.downloadPath }.flatten())
actionMode?.finish() // clearCheckedItems()
// actionMode?.finish()
true true
} }
R.id.select_all -> { R.id.select_all -> {
historyAdapter?.checkAll(historyList) //TODO
selectedObjects.clear() // historyAdapter.checkAll()
historyList?.forEach { selectedObjects.add(it!!) } // historyList?.forEach { selectedObjects.add(it!!) }
mode?.title = "(${selectedObjects.size}) ${resources.getString(R.string.all_items_selected)}" // mode?.title = "(${selectedObjects.size}) ${resources.getString(R.string.all_items_selected)}"
true true
} }
R.id.invert_selected -> { R.id.invert_selected -> {
historyAdapter?.invertSelected(historyList) //TODO
val invertedList = arrayListOf<HistoryItem>() // historyAdapter.invertSelected()
historyList?.forEach { // val invertedList = arrayListOf<HistoryItem>()
if (!selectedObjects.contains(it)!!) invertedList.add(it!!) // historyList?.forEach {
} // if (!selectedObjects.contains(it)!!) invertedList.add(it!!)
selectedObjects.clear() // }
selectedObjects.addAll(invertedList) // selectedObjects.clear()
actionMode!!.title = "${selectedObjects.size} ${getString(R.string.selected)}" // selectedObjects.addAll(invertedList)
if (invertedList.isEmpty()) actionMode?.finish() // actionMode!!.title = "${selectedObjects.size} ${getString(R.string.selected)}"
// if (invertedList.isEmpty()) actionMode?.finish()
true true
} }
else -> false else -> false
@ -654,15 +642,23 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
override fun onDestroyActionMode(mode: ActionMode?) { override fun onDestroyActionMode(mode: ActionMode?) {
actionMode = null actionMode = null
(activity as MainActivity).enableBottomNavigation() (activity as MainActivity).enableBottomNavigation()
clearCheckedItems() historyAdapter.clearCheckedItems()
topAppBar!!.menu.forEach { it.isEnabled = true } topAppBar.menu.forEach { it.isEnabled = true }
}
suspend fun getSelectedIDs() : List<Long>{
//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 = private var simpleCallback: ItemTouchHelper.SimpleCallback =
object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) { 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) { override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val itemID = viewHolder.itemView.tag.toString().toLong()
val position = viewHolder.bindingAdapterPosition val position = viewHolder.bindingAdapterPosition
when (direction) { when (direction) {
ItemTouchHelper.LEFT -> { ItemTouchHelper.LEFT -> {
val deletedItem = historyList!![position] lifecycleScope.launch {
historyAdapter!!.notifyItemChanged(position) val deletedItem = withContext(Dispatchers.IO) {
UiUtil.showRemoveHistoryItemDialog(deletedItem!!, requireActivity(), historyViewModel.getByID(itemID)
delete = { item, deleteFile -> }
lifecycleScope.launch { historyAdapter.notifyItemChanged(position)
withContext(Dispatchers.IO){ UiUtil.showRemoveHistoryItemDialog(deletedItem, requireActivity(),
historyViewModel.delete(item, deleteFile) delete = { item, deleteFile ->
} lifecycleScope.launch {
withContext(Dispatchers.IO){
historyViewModel.delete(item, deleteFile)
}
if (!deleteFile){ if (!deleteFile){
Snackbar.make(recyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_INDEFINITE) Snackbar.make(recyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_INDEFINITE)
.setAction(getString(R.string.undo)) { .setAction(getString(R.string.undo)) {
historyViewModel.insert(deletedItem) historyViewModel.insert(deletedItem)
}.show() }.show()
}
} }
} }
}) )
}
} }
ItemTouchHelper.RIGHT -> { ItemTouchHelper.RIGHT -> {
val item = historyList!![position]!! lifecycleScope.launch {
historyAdapter!!.notifyItemChanged(position) val item = withContext(Dispatchers.IO) {
findNavController().navigate(R.id.downloadBottomSheetDialog, bundleOf( historyViewModel.getByID(itemID)
Pair("result", downloadViewModel.createResultItemFromHistory(item)), }
Pair("type", item.type) 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( super.onChildDraw(
c, c,
recyclerView, recyclerView,
viewHolder!!, viewHolder,
dX, dX,
dY, dY,
actionState, actionState,
@ -748,7 +754,13 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
override fun onButtonClick(itemID: Long, isPresent: Boolean) { override fun onButtonClick(itemID: Long, isPresent: Boolean) {
if (isPresent){ 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 { companion object {

View file

@ -49,6 +49,12 @@ class UpdateSettingsFragment : BaseSettingsFragment() {
true true
} }
ytdlVersion!!.setOnPreferenceClickListener {
initYTDLUpdate(editor)
true
}
updateYTDL!!.onPreferenceClickListener = updateYTDL!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener { Preference.OnPreferenceClickListener {
initYTDLUpdate(editor) initYTDLUpdate(editor)

View file

@ -46,9 +46,6 @@ import com.deniscerri.ytdl.database.models.Format
import com.deniscerri.ytdl.database.models.observeSources.ObserveSourcesItem import com.deniscerri.ytdl.database.models.observeSources.ObserveSourcesItem
import com.deniscerri.ytdl.database.repository.DownloadRepository import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.repository.ObserveSourcesRepository.EveryCategory 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.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.tabs.TabLayout 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.neo.highlight.util.scheme.ColorScheme
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -526,11 +524,43 @@ object Extensions {
} }
fun String.extractURL() : String { fun String.extractURL() : String {
val res = Pattern.compile("(http|ftp|https)://([\\w_-]+(?:\\.[\\w_-]+)+)([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])").matcher(this) val res =
return if (res.find()){ Pattern.compile("(http|ftp|https)://([\\w_-]+(?:\\.[\\w_-]+)+)([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])")
.matcher(this)
return if (res.find()) {
res.group() res.group()
}else{ } else {
this this
} }
} }
fun String.isURL(): Boolean {
return Pattern.compile("(http|ftp|https)://([\\w_-]+(?:\\.[\\w_-]+)+)([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])").matcher(this).find()
}
fun <T1, T2, T3, T4, T5, T6, T7, R> combine(
flow: Flow<T1>,
flow2: Flow<T2>,
flow3: Flow<T3>,
flow4: Flow<T4>,
flow5: Flow<T5>,
flow6: Flow<T6>,
flow7: Flow<T7>,
transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R
): Flow<R> = 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
)
}
} }

View file

@ -191,6 +191,7 @@ object UiUtil {
val extraCommandsSwitch : MaterialSwitch = bottomSheet.findViewById(R.id.extraCommandsSwitch)!! val extraCommandsSwitch : MaterialSwitch = bottomSheet.findViewById(R.id.extraCommandsSwitch)!!
val extraCommandsAudio : CheckBox = bottomSheet.findViewById(R.id.checkbox_audio)!! val extraCommandsAudio : CheckBox = bottomSheet.findViewById(R.id.checkbox_audio)!!
val extraCommandsVideo : CheckBox = bottomSheet.findViewById(R.id.checkbox_video)!! 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 shortcutsChipGroup : ChipGroup = bottomSheet.findViewById(R.id.shortcutsChipGroup)!!
val editShortcuts : Button = bottomSheet.findViewById(R.id.edit_shortcuts)!! val editShortcuts : Button = bottomSheet.findViewById(R.id.edit_shortcuts)!!
@ -252,6 +253,7 @@ object UiUtil {
if (item != null){ if (item != null){
preferredCommandSwitch.isChecked = item.content == sharedPreferences.getString("preferred_command_template", "") preferredCommandSwitch.isChecked = item.content == sharedPreferences.getString("preferred_command_template", "")
extraCommandsDataFetchingSwitch.isChecked = item.useAsExtraCommandDataFetching
extraCommandsSwitch.isChecked = item.useAsExtraCommand extraCommandsSwitch.isChecked = item.useAsExtraCommand
if (item.useAsExtraCommand){ if (item.useAsExtraCommand){
@ -307,7 +309,7 @@ object UiUtil {
if (item == null){ 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) commandTemplateViewModel.insert(t)
newTemplate(t) newTemplate(t)
}else{ }else{
@ -316,6 +318,7 @@ object UiUtil {
item.useAsExtraCommand = extraCommandsSwitch.isChecked item.useAsExtraCommand = extraCommandsSwitch.isChecked
item.useAsExtraCommandAudio = extraCommandsAudio.isChecked item.useAsExtraCommandAudio = extraCommandsAudio.isChecked
item.useAsExtraCommandVideo = extraCommandsVideo.isChecked item.useAsExtraCommandVideo = extraCommandsVideo.isChecked
item.useAsExtraCommandDataFetching = extraCommandsDataFetchingSwitch.isChecked
commandTemplateViewModel.update(item) commandTemplateViewModel.update(item)
newTemplate(item) newTemplate(item)
} }

View file

@ -113,8 +113,8 @@ class DownloadWorker(
setForegroundAsync(ForegroundInfo(notificationID, workNotif)) setForegroundAsync(ForegroundInfo(notificationID, workNotif))
} }
queuedItems.collectLatest { items -> queuedItems.collect { items ->
if (this@DownloadWorker.isStopped) return@collectLatest if (this@DownloadWorker.isStopped) return@collect
runningYTDLInstances.clear() runningYTDLInstances.clear()
val activeDownloads = dao.getActiveDownloadsList() val activeDownloads = dao.getActiveDownloadsList()
@ -126,19 +126,19 @@ class DownloadWorker(
val useScheduler = sharedPreferences.getBoolean("use_scheduler", false) val useScheduler = sharedPreferences.getBoolean("use_scheduler", false)
if (items.isEmpty() && running.isEmpty()) { if (items.isEmpty() && running.isEmpty()) {
WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id) WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id)
return@collectLatest return@collect
} }
if (useScheduler){ if (useScheduler){
if (items.none{it.downloadStartTime > 0L} && running.isEmpty() && !alarmScheduler.isDuringTheScheduledTime()) { if (items.none{it.downloadStartTime > 0L} && running.isEmpty() && !alarmScheduler.isDuringTheScheduledTime()) {
WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id) WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id)
return@collectLatest return@collect
} }
} }
if (priorityItemIDs.isEmpty() && !continueAfterPriorityIds) { if (priorityItemIDs.isEmpty() && !continueAfterPriorityIds) {
WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id) WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id)
return@collectLatest return@collect
} }
val concurrentDownloads = sharedPreferences.getInt("concurrent_downloads", 1) - running.size val concurrentDownloads = sharedPreferences.getInt("concurrent_downloads", 1) - running.size
@ -312,6 +312,7 @@ class DownloadWorker(
finalPaths, finalPaths,
downloadItem.website, downloadItem.website,
downloadItem.format, downloadItem.format,
downloadItem.format.filesize,
downloadItem.id, downloadItem.id,
commandString) commandString)
historyDao.insert(historyItem) historyDao.insert(historyItem)

View file

@ -157,6 +157,20 @@
</LinearLayout> </LinearLayout>
<View
style="@style/Divider.Horizontal"
android:layout_marginVertical="10dp" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/extraCommandDataFetching"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginHorizontal="10dp"
android:checked="false"
android:text="@string/data_fetching_extra_command"
android:textSize="15sp"
app:layout_constraintEnd_toEndOf="parent" />
<View <View
style="@style/Divider.Horizontal" style="@style/Divider.Horizontal"
android:layout_marginVertical="10dp" /> android:layout_marginVertical="10dp" />

View file

@ -434,6 +434,6 @@
<string name="socket_timeout">Socket Timeout</string> <string name="socket_timeout">Socket Timeout</string>
<string name="socket_timeout_description">Time to wait before giving up, in seconds</string> <string name="socket_timeout_description">Time to wait before giving up, in seconds</string>
<string name="advanced">Advanced</string> <string name="advanced">Advanced</string>
<string name="data_fetching_extra_commands">Data Fetching Extra Commands</string> <string name="data_fetching_extra_command">Data Fetching Extra Command</string>
<string name="thumbnail">Thumbnail</string> <string name="thumbnail">Thumbnail</string>
</resources> </resources>

View file

@ -2,32 +2,27 @@
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<EditTextPreference <PreferenceCategory android:title="YouTube">
android:defaultValue="default,mediaconnect" <EditTextPreference
android:icon="@drawable/ic_code" android:defaultValue="default,mediaconnect"
android:key="youtube_player_client" android:icon="@drawable/ic_code"
app:useSimpleSummaryProvider="true" android:key="youtube_player_client"
android:title="Player Client" /> app:useSimpleSummaryProvider="true"
android:title="Player Client" />
<EditTextPreference <EditTextPreference
android:defaultValue="" android:defaultValue=""
android:icon="@drawable/ic_code" android:icon="@drawable/ic_code"
android:key="youtube_po_token" android:key="youtube_po_token"
app:useSimpleSummaryProvider="true" app:useSimpleSummaryProvider="true"
android:title="PO Token" /> android:title="PO Token" />
<EditTextPreference <EditTextPreference
android:defaultValue="" android:defaultValue=""
android:icon="@drawable/ic_code" android:icon="@drawable/ic_code"
android:key="youtube_other_extractor_args" android:key="youtube_other_extractor_args"
app:useSimpleSummaryProvider="true" app:useSimpleSummaryProvider="true"
android:title="@string/other_youtube_extractor_args" /> android:title="@string/other_youtube_extractor_args" />
</PreferenceCategory>
<EditTextPreference
android:defaultValue=""
android:icon="@drawable/ic_terminal"
android:key="data_fetching_extra_commands"
app:useSimpleSummaryProvider="true"
android:title="@string/data_fetching_extra_commands" />
</PreferenceScreen> </PreferenceScreen>

View file

@ -2,6 +2,11 @@
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<Preference
app:icon="@drawable/ic_info"
app:key="version"
app:title="@string/version"/>
<PreferenceCategory android:title="@string/source"> <PreferenceCategory android:title="@string/source">
<Preference <Preference
@ -35,6 +40,36 @@
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory android:title="@string/app">
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
android:icon="@drawable/ic_update_app"
android:key="update_app"
app:summary="@string/update_app_summary"
app:title="@string/update_app" />
<EditTextPreference
app:isPreferenceVisible="false"
app:key="skip_updates"
app:defaultValue="" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:icon="@drawable/ic_experiment"
android:key="update_beta"
app:summary="@string/update_app_beta_summary"
app:title="@string/update_app_beta" />
<Preference
app:icon="@drawable/ic_chapters"
app:key="changelog"
app:title="@string/changelog" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/format"> <PreferenceCategory android:title="@string/format">
<ListPreference <ListPreference
android:defaultValue="filesize" android:defaultValue="filesize"
@ -63,41 +98,4 @@
app:title="@string/update_formats" /> app:title="@string/update_formats" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory android:title="@string/app">
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
android:icon="@drawable/ic_update_app"
android:key="update_app"
app:summary="@string/update_app_summary"
app:title="@string/update_app" />
<EditTextPreference
app:isPreferenceVisible="false"
app:key="skip_updates"
app:defaultValue="" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:icon="@drawable/ic_experiment"
android:key="update_beta"
app:summary="@string/update_app_beta_summary"
app:title="@string/update_app_beta" />
<Preference
app:icon="@drawable/ic_chapters"
app:key="changelog"
app:title="@string/changelog" />
<Preference
app:icon="@drawable/ic_info"
app:key="version"
app:title="@string/version"/>
</PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>