From dd3578bc4872660afb4c3fa6ef1112d7cfc04bc2 Mon Sep 17 00:00:00 2001 From: zaednasr <75589932+zaednasr@users.noreply.github.com> Date: Sun, 14 Jan 2024 12:23:49 +0100 Subject: [PATCH] 1.7.2 pre - Increased the height of the history card a slight bit - Fixed app not using the -S format sorter in normal GUI downloads - Added the multiple download card as an option to disable the swipe gestures - Added a recyclerview to the search results because they were buggy before and would not show sometimes - Added a new history item card design in cases if you quick-downloaded a playlist or if you made multiple cuts of a file. Instead of being separate, they are bundled and you can share them all at once and if you want to open the file a new dialog will show telling you to choose which one. If any of them is deleted, they are greyed out - Fixed the shortcuts behaviour by allowing duplicate shortcuts to be added to textbox and to not check - Added format filtering in the format list bottom sheet. You can filter between ALL, SUGGESTED, SMALLEST, GENERIC. Suggested formats are built upon your app preferences like your preferred codec, container, quality. Smallest formats are all formats for each resolution with the smallest filesize. Generic are formats you see if an item has not updated the formats yet, in case you want to use those for some reason - Added clipboard button right in the home screen for quick access to the url you copied instead of going to the searchview. That item is also available there aswell - Added ability to show the URL in a history item if the download produced no title for it - Added zoom-in zoom-out slider for logs and terminal screens. - Added more options of datafetching for titles and authors, like track, alt_title to avoid the default youtube naming scheme when available. Useful for audio downloads - Since some people are confused. In the download card each tab item is independent and they each create a distinct download item and when you press download the app picks from the tab you are currently. If you want to select the audio format in the video tab you can just open the format list and scroll to the bottom for the audio formats. You can select multiple if you want and then press ok. But since some people thought that selecting the audio in audio tab would apply to the video tab, i added the ability to synchronise that format automatically in case people dont know about this lol - Other small bug fixes --- .gitignore | 3 +- app/build.gradle | 2 +- .../14.json | 598 ++++++++++++++++++ .../deniscerri/ytdlnis/database/Converters.kt | 6 + .../deniscerri/ytdlnis/database/DBManager.kt | 6 +- .../deniscerri/ytdlnis/database/Migrations.kt | 57 ++ .../ytdlnis/database/dao/DownloadDao.kt | 3 + .../ytdlnis/database/models/HistoryItem.kt | 2 +- .../database/models/SearchSuggestionItem.kt | 15 + .../database/repository/DownloadRepository.kt | 4 + .../database/repository/HistoryRepository.kt | 10 +- .../database/viewmodel/DownloadViewModel.kt | 59 +- .../database/viewmodel/ResultViewModel.kt | 2 +- .../com/deniscerri/ytdlnis/ui/HomeFragment.kt | 326 +++++----- .../ytdlnis/ui/adapter/HistoryAdapter.kt | 63 +- .../ui/adapter/SearchSuggestionsAdapter.kt | 126 ++++ .../ConfigureDownloadBottomSheetDialog.kt | 5 +- .../ui/downloadcard/DownloadAudioFragment.kt | 7 +- .../downloadcard/DownloadBottomSheetDialog.kt | 15 +- .../downloadcard/DownloadCommandFragment.kt | 13 +- .../DownloadMultipleBottomSheetDialog.kt | 8 +- .../ui/downloadcard/DownloadVideoFragment.kt | 27 +- .../FormatSelectionBottomSheetDialog.kt | 233 ++++++- .../ytdlnis/ui/downloads/HistoryFragment.kt | 6 +- .../more/downloadLogs/DownloadLogFragment.kt | 31 +- .../ui/more/settings/MainSettingsFragment.kt | 12 +- .../ui/more/terminal/TerminalFragment.kt | 23 +- .../com/deniscerri/ytdlnis/util/Extensions.kt | 29 + .../com/deniscerri/ytdlnis/util/FileUtil.kt | 2 +- .../com/deniscerri/ytdlnis/util/InfoUtil.kt | 117 ++-- .../ytdlnis/util/NotificationUtil.kt | 2 +- .../com/deniscerri/ytdlnis/util/UiUtil.kt | 110 +++- .../deniscerri/ytdlnis/work/DownloadWorker.kt | 37 +- .../baseline_align_horizontal_left_24.xml | 5 + .../res/drawable/baseline_expand_less_24.xml | 5 + .../res/drawable/baseline_expand_more_24.xml | 5 + .../main/res/layout-land/fragment_home.xml | 181 +++--- .../res/layout-land/result_card_details.xml | 186 ------ .../res/layout-sw600dp-land/fragment_home.xml | 182 +++--- .../main/res/layout-sw600dp/fragment_home.xml | 181 +++--- .../res/layout-sw950dp-land/fragment_home.xml | 183 +++--- app/src/main/res/layout/activity_terminal.xml | 12 + .../main/res/layout/download_bottom_sheet.xml | 10 +- .../res/layout/filename_template_dialog.xml | 91 +-- app/src/main/res/layout/filepath_card.xml | 109 ++++ app/src/main/res/layout/filepathlist.xml | 67 ++ .../main/res/layout/format_category_sheet.xml | 76 +++ .../res/layout/format_select_bottom_sheet.xml | 79 ++- .../main/res/layout/fragment_download_log.xml | 9 + app/src/main/res/layout/fragment_home.xml | 185 +++--- app/src/main/res/layout/fragment_terminal.xml | 4 +- app/src/main/res/layout/history_card.xml | 4 +- .../main/res/layout/history_card_multiple.xml | 174 +++++ .../main/res/layout/result_card_details.xml | 341 +++++----- .../res/layout/search_suggestion_item.xml | 1 + app/src/main/res/layout/subtitle_dialog.xml | 89 +-- app/src/main/res/menu/log_detail_menu.xml | 8 +- app/src/main/res/menu/terminal_top_menu.xml | 6 + app/src/main/res/navigation/nav_graph.xml | 10 +- app/src/main/res/values-ar/strings.xml | 2 +- app/src/main/res/values-az/strings.xml | 2 +- app/src/main/res/values-bn-rIN/strings.xml | 2 +- app/src/main/res/values-bn/strings.xml | 2 +- app/src/main/res/values-cs/strings.xml | 2 +- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-el/strings.xml | 2 +- app/src/main/res/values-es/strings.xml | 2 +- app/src/main/res/values-fa/strings.xml | 2 +- app/src/main/res/values-fr/strings.xml | 2 +- app/src/main/res/values-hi/strings.xml | 2 +- app/src/main/res/values-hu/strings.xml | 2 +- app/src/main/res/values-in/strings.xml | 2 +- app/src/main/res/values-it/strings.xml | 2 +- app/src/main/res/values-iw/strings.xml | 2 +- app/src/main/res/values-ja/strings.xml | 2 +- app/src/main/res/values-ko/strings.xml | 2 +- app/src/main/res/values-lv/strings.xml | 2 +- app/src/main/res/values-nb-rNO/strings.xml | 2 +- app/src/main/res/values-nl/strings.xml | 2 +- app/src/main/res/values-nn/strings.xml | 2 +- app/src/main/res/values-pa/strings.xml | 2 +- app/src/main/res/values-pl/strings.xml | 2 +- app/src/main/res/values-pt-rBR/strings.xml | 2 +- app/src/main/res/values-pt/strings.xml | 2 +- app/src/main/res/values-ro/strings.xml | 2 +- app/src/main/res/values-ru/strings.xml | 2 +- app/src/main/res/values-sk/strings.xml | 2 +- app/src/main/res/values-sq-rAL/strings.xml | 2 +- app/src/main/res/values-sr/strings.xml | 2 +- app/src/main/res/values-th/strings.xml | 2 +- app/src/main/res/values-tr-rTR/strings.xml | 2 +- app/src/main/res/values-uk/strings.xml | 2 +- app/src/main/res/values-vi/strings.xml | 2 +- app/src/main/res/values-zh-rCN/strings.xml | 2 +- app/src/main/res/values-zh-rTW/strings.xml | 2 +- app/src/main/res/values/arrays.xml | 15 + app/src/main/res/values/strings.xml | 13 +- app/src/main/res/xml/updating_preferences.xml | 11 +- .../android/en-US/changelogs/10720.txt | 17 + 99 files changed, 2854 insertions(+), 1436 deletions(-) create mode 100644 app/schemas/com.deniscerri.ytdlnis.database.DBManager/14.json create mode 100644 app/src/main/java/com/deniscerri/ytdlnis/database/Migrations.kt create mode 100644 app/src/main/java/com/deniscerri/ytdlnis/database/models/SearchSuggestionItem.kt create mode 100644 app/src/main/java/com/deniscerri/ytdlnis/ui/adapter/SearchSuggestionsAdapter.kt create mode 100644 app/src/main/res/drawable/baseline_align_horizontal_left_24.xml create mode 100644 app/src/main/res/drawable/baseline_expand_less_24.xml create mode 100644 app/src/main/res/drawable/baseline_expand_more_24.xml delete mode 100644 app/src/main/res/layout-land/result_card_details.xml create mode 100644 app/src/main/res/layout/filepath_card.xml create mode 100644 app/src/main/res/layout/filepathlist.xml create mode 100644 app/src/main/res/layout/format_category_sheet.xml create mode 100644 app/src/main/res/layout/history_card_multiple.xml create mode 100644 fastlane/metadata/android/en-US/changelogs/10720.txt diff --git a/.gitignore b/.gitignore index f5b31588..3b4b454b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ /build /captures .externalNativeBuild -/benchmark \ No newline at end of file +/benchmark +*.png \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index a5771af0..5304dbf3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,7 +10,7 @@ plugins { def properties = new Properties() def versionMajor = 1 def versionMinor = 7 -def versionPatch = 1 +def versionPatch = 2 def versionBuild = 0 // bump for dogfood builds, public betas, etc. def versionExt = "" diff --git a/app/schemas/com.deniscerri.ytdlnis.database.DBManager/14.json b/app/schemas/com.deniscerri.ytdlnis.database.DBManager/14.json new file mode 100644 index 00000000..1a7044f5 --- /dev/null +++ b/app/schemas/com.deniscerri.ytdlnis.database.DBManager/14.json @@ -0,0 +1,598 @@ +{ + "formatVersion": 1, + "database": { + "version": 14, + "identityHash": "5cea521e1a26473b2ec5298791f10ac4", + "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, `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": "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)", + "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 + } + ], + "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)", + "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" + } + ], + "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": [] + } + ], + "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, '5cea521e1a26473b2ec5298791f10ac4')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/Converters.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/Converters.kt index 14711a5f..1e06646f 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/Converters.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/Converters.kt @@ -15,6 +15,12 @@ class Converters { @TypeConverter fun listOfFormatsToString(list: List?) = Gson().toJson(list).toString() + @TypeConverter + fun stringToListOfStrings(value: String) = Gson().fromJson(value, Array::class.java).toList() + + @TypeConverter + fun listOfStringsToString(list: List) = Gson().toJson(list).toString() + @TypeConverter fun formatToString(format: Format): String = Gson().toJson(format) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/DBManager.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/DBManager.kt index d3e6cebd..847460f5 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/DBManager.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/DBManager.kt @@ -23,7 +23,7 @@ import com.deniscerri.ytdlnis.database.models.* LogItem::class, TerminalItem::class ], - version = 13, + version = 14, autoMigrations = [ AutoMigration (from = 1, to = 2), AutoMigration (from = 2, to = 3), @@ -36,7 +36,8 @@ import com.deniscerri.ytdlnis.database.models.* AutoMigration (from = 9, to = 10), AutoMigration (from = 10, to = 11), AutoMigration (from = 11, to = 12), - AutoMigration (from = 12, to = 13) + AutoMigration (from = 12, to = 13), + //AutoMigration (from = 13, to = 14), MANUALLY HANDLED ] ) abstract class DBManager : RoomDatabase(){ @@ -66,6 +67,7 @@ abstract class DBManager : RoomDatabase(){ DBManager::class.java, "YTDLnisDatabase" ) + .addMigrations(*Migrations.migrationList) .build() instance = dbInstance dbInstance diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/Migrations.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/Migrations.kt new file mode 100644 index 00000000..0943ca2b --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/Migrations.kt @@ -0,0 +1,57 @@ +package com.deniscerri.ytdlnis.database + +import android.annotation.SuppressLint +import android.app.Dialog +import android.content.Context +import android.content.res.Resources +import android.graphics.Color +import android.graphics.Outline +import android.graphics.drawable.ShapeDrawable +import android.graphics.drawable.shapes.OvalShape +import android.media.MediaMetadataRetriever +import android.media.MediaMetadataRetriever.METADATA_KEY_DURATION +import android.net.Uri +import android.util.DisplayMetrics +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.view.ViewOutlineProvider +import android.widget.EditText +import android.widget.TextView +import androidx.annotation.Px +import androidx.core.view.updateLayoutParams +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.withStarted +import androidx.recyclerview.widget.RecyclerView +import androidx.room.migration.Migration +import androidx.work.impl.Migration_12_13 +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.neo.highlight.core.Highlight +import com.neo.highlight.util.listener.HighlightTextWatcher +import com.neo.highlight.util.scheme.ColorScheme +import kotlinx.coroutines.launch +import me.zhanghai.android.fastscroll.FastScrollerBuilder +import java.io.File +import java.util.regex.Pattern + + +object Migrations { + + @SuppressLint("Range") + val migrationList = arrayOf( + //Moving from one file path to multiple file paths of a history item + Migration(13, 14){database -> + val cursor = database.query("SELECT * FROM history") + while(cursor.moveToNext()) { + val id = cursor.getLong(cursor.getColumnIndex("id")) + val path = cursor.getString(cursor.getColumnIndex("downloadPath")) + val newPath = "[\"${path.replace("\"", "\\\"").replace("'", "''")}\"]" + database.execSQL("UPDATE history SET downloadPath = '${newPath}' WHERE id = $id") + } + } + ) + + + +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/DownloadDao.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/DownloadDao.kt index fc9206b9..413c1613 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/DownloadDao.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/DownloadDao.kt @@ -138,6 +138,9 @@ interface DownloadDao { @Upsert suspend fun update(item: DownloadItem) + @Update + suspend fun updateWithoutUpsert(item: DownloadItem) + @Query("UPDATE downloads SET logID=null") fun removeAllLogID() diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/models/HistoryItem.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/models/HistoryItem.kt index 23f67300..b8b885ce 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/models/HistoryItem.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/models/HistoryItem.kt @@ -16,7 +16,7 @@ data class HistoryItem( val thumb: String, val type: DownloadViewModel.Type, val time: Long, - val downloadPath: String, + val downloadPath: List, val website: String, val format: Format, @ColumnInfo(defaultValue = "0") diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/models/SearchSuggestionItem.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/models/SearchSuggestionItem.kt new file mode 100644 index 00000000..0f1bf51c --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/models/SearchSuggestionItem.kt @@ -0,0 +1,15 @@ +package com.deniscerri.ytdlnis.database.models + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel + +data class SearchSuggestionItem( + var text: String, + val type: SearchSuggestionType, +) + +enum class SearchSuggestionType{ + SUGGESTION, HISTORY, CLIPBOARD +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/repository/DownloadRepository.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/repository/DownloadRepository.kt index 658fd24f..2e9e390c 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/repository/DownloadRepository.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/repository/DownloadRepository.kt @@ -66,6 +66,10 @@ class DownloadRepository(private val downloadDao: DownloadDao) { downloadDao.update(item) } + suspend fun updateWithoutUpsert(item: DownloadItem){ + kotlin.runCatching { downloadDao.updateWithoutUpsert(item) } + } + suspend fun setDownloadStatus(item: DownloadItem, status: Status){ item.status = status.toString() diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/repository/HistoryRepository.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/repository/HistoryRepository.kt index ffb8eca7..54cc9fd5 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/repository/HistoryRepository.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/repository/HistoryRepository.kt @@ -48,14 +48,18 @@ class HistoryRepository(private val historyDao: HistoryDao) { suspend fun delete(item: HistoryItem, deleteFile: Boolean){ historyDao.delete(item.id) if (deleteFile){ - FileUtil.deleteFile(item.downloadPath) + item.downloadPath.forEach { + FileUtil.deleteFile(it) + } } } suspend fun deleteAll(deleteFile: Boolean = false){ if (deleteFile){ historyDao.getAllHistoryList().forEach { item -> - FileUtil.deleteFile(item.downloadPath) + item.downloadPath.forEach { + FileUtil.deleteFile(it) + } } } historyDao.deleteAll() @@ -72,7 +76,7 @@ class HistoryRepository(private val historyDao: HistoryDao) { suspend fun clearDeletedHistory(){ items.collectLatest { it.forEach { item -> - if (!FileUtil.exists(item.downloadPath)){ + if (item.downloadPath.all { path -> !FileUtil.exists(path) }){ historyDao.delete(item.id) } } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/DownloadViewModel.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/DownloadViewModel.kt index a6a55891..80648819 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/DownloadViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/DownloadViewModel.kt @@ -1,5 +1,6 @@ package com.deniscerri.ytdlnis.database.viewmodel +import android.annotation.SuppressLint import android.app.Application import android.content.Context import android.content.SharedPreferences @@ -424,8 +425,15 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application val audioPreferences = AudioPreferences(embedThumb, cropThumb,false, ArrayList(sponsorblock!!)) val videoPreferences = VideoPreferences(embedSubs, addChapters, false, ArrayList(sponsorblock), saveSubs) - val downloadPath = File(historyItem.downloadPath) - val path = if (downloadPath.exists()) downloadPath.parent else defaultPath + var path = defaultPath + historyItem.downloadPath.first().apply { + File(this).parent?.apply { + if (File(this).exists()){ + path = this + } + } + + } return DownloadItem(0, historyItem.url, historyItem.title, @@ -443,23 +451,37 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application } + fun getPreferredAudioRequirements(): MutableList<(Format) -> Boolean> { + val requirements: MutableList<(Format) -> Boolean> = mutableListOf() + requirements.add {it: Format -> audioFormatIDPreference.contains(it.format_id)} + + sharedPreferences.getString("audio_language", "")?.apply { + if (this.isNotBlank()){ + requirements.add { it: Format -> it.lang == this } + } + } + + requirements.add {it: Format -> it.container == audioContainer } + requirements.add {it: Format -> "^(${audioCodec}).+$".toRegex(RegexOption.IGNORE_CASE).matches(it.acodec)} + return requirements + } + + fun getPreferredVideoRequirements(): MutableList<(Format) -> Boolean> { + val requirements: MutableList<(Format) -> Boolean> = mutableListOf() + requirements.add { it: Format -> formatIDPreference.contains(it.format_id) } + requirements.add { it: Format -> if (videoContainer == "mp4") it.container.equals("mpeg_4", true) else it.container.equals(videoContainer, true)} + requirements.add { it: Format -> it.format_note.contains(videoQualityPreference.split("_")[0].dropLast(1)) } + requirements.add { it: Format -> "^(${videoCodec}).+$".toRegex(RegexOption.IGNORE_CASE).matches(it.vcodec)} + return requirements + } + fun getFormat(formats: List, type: Type) : Format { when(type) { Type.audio -> { return cloneFormat ( try { val theFormats = formats.filter { it.format_note.contains("audio", ignoreCase = true) } - val requirements: MutableList<(Format) -> Boolean> = mutableListOf() - requirements.add {it: Format -> audioFormatIDPreference.contains(it.format_id)} - - sharedPreferences.getString("audio_language", "")?.apply { - if (this.isNotBlank()){ - requirements.add { it: Format -> it.lang == this } - } - } - - requirements.add {it: Format -> it.container == audioContainer } - requirements.add {it: Format -> "^(${audioCodec}).+$".toRegex(RegexOption.IGNORE_CASE).matches(it.acodec)} + val requirements = getPreferredAudioRequirements() theFormats.maxByOrNull { f -> requirements.count{req -> req(f)} } ?: throw Exception() }catch (e: Exception){ bestAudioFormat @@ -476,11 +498,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application theFormats.last() } else /*best*/ -> { - val requirements: MutableList<(Format) -> Boolean> = mutableListOf() - requirements.add { it: Format -> formatIDPreference.contains(it.format_id) } - requirements.add { it: Format -> if (videoContainer == "mp4") it.container.equals("mpeg_4", true) else it.container.equals(videoContainer, true)} - requirements.add { it: Format -> it.format_note.contains(videoQualityPreference.split("_")[0].dropLast(1)) } - requirements.add { it: Format -> "^(${videoCodec}).+$".toRegex(RegexOption.IGNORE_CASE).matches(it.vcodec)} + val requirements = getPreferredVideoRequirements() theFormats.maxByOrNull { f -> requirements.count{ req -> req(f)} } ?: throw Exception() } } @@ -677,7 +695,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application }else{ //check if downloaded and file exists val history = withContext(Dispatchers.IO){ - historyRepository.getAllByURL(it.url).filter { item -> FileUtil.exists(item.downloadPath) } + historyRepository.getAllByURL(it.url).filter { item -> item.downloadPath.any { path -> FileUtil.exists(path) } } } val existingHistory = history.firstOrNull { @@ -733,6 +751,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application } } + @SuppressLint("RestrictedApi") private suspend fun startDownloadWorker(queuedItems: List) { val context = App.instance val allowMeteredNetworks = sharedPreferences.getBoolean("metered_networks", true) @@ -812,7 +831,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application if (downloadItem.thumb.isEmpty()) downloadItem.thumb = info?.thumb.toString() runBlocking { wasQuickDownloaded = resultDao.getCountInt() == 0 - dao.update(downloadItem) + repository.updateWithoutUpsert(downloadItem) } } } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/ResultViewModel.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/ResultViewModel.kt index fbea5868..ef11f50d 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/ResultViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/ResultViewModel.kt @@ -116,7 +116,7 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a } } - if (!isForegrounded()){ + if (!isForegrounded() && inputQueries.size > 1){ notificationUtil.showQueriesFinished() } uiState.update {it.copy(processing = false)} diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/HomeFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/HomeFragment.kt index 56289ed2..ec4a4669 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/HomeFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/HomeFragment.kt @@ -28,18 +28,21 @@ import androidx.core.widget.doAfterTextChanged import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope -import androidx.navigation.NavDestination import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceManager import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.deniscerri.ytdlnis.MainActivity import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.ResultItem +import com.deniscerri.ytdlnis.database.models.SearchSuggestionItem +import com.deniscerri.ytdlnis.database.models.SearchSuggestionType import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel import com.deniscerri.ytdlnis.ui.adapter.HomeAdapter +import com.deniscerri.ytdlnis.ui.adapter.SearchSuggestionsAdapter import com.deniscerri.ytdlnis.util.Extensions.enableFastScroll import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.ThemeUtil @@ -53,6 +56,7 @@ import com.google.android.material.chip.ChipGroup import com.google.android.material.color.MaterialColors import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton +import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.search.SearchBar import com.google.android.material.search.SearchView import kotlinx.coroutines.CoroutineScope @@ -64,17 +68,15 @@ import kotlinx.coroutines.withContext import java.util.* -class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListener { +class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggestionsAdapter.OnItemClickListener, OnClickListener { private var inputQueries: MutableList? = null private var homeAdapter: HomeAdapter? = null + private var searchSuggestionsAdapter: SearchSuggestionsAdapter? = null + private var queriesConstraint: ConstraintLayout? = null - private var searchSuggestions: ScrollView? = null - private var searchSuggestionsLinearLayout: LinearLayout? = null - private var searchHistory: ScrollView? = null - private var searchHistoryLinearLayout: LinearLayout? = null - - private var downloadFabs: CoordinatorLayout? = null - private var downloadAllFabCoordinator: CoordinatorLayout? = null + private var downloadSelectedFab: ExtendedFloatingActionButton? = null + private var downloadAllFab: ExtendedFloatingActionButton? = null + private var clipboardFab: FloatingActionButton? = null private var homeFabs: CoordinatorLayout? = null private var infoUtil: InfoUtil? = null private var downloadQueue: ArrayList? = null @@ -92,9 +94,9 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene private var searchView: SearchView? = null private var providersChipGroup: ChipGroup? = null private var chipGroupDivider: View? = null - private var linkYouCopied: ConstraintLayout? = null private var queriesChipGroup: ChipGroup? = null private var recyclerView: RecyclerView? = null + private var searchSuggestionsRecyclerView: RecyclerView? = null private var uiHandler: Handler? = null private var resultsList: List? = null private var selectedObjects: ArrayList? = null @@ -136,17 +138,14 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene shimmerCards = view.findViewById(R.id.shimmer_results_framelayout) searchBar = view.findViewById(R.id.search_bar) searchView = view.findViewById(R.id.search_view) - linkYouCopied = searchView?.findViewById(R.id.link_you_copied) appBarLayout = view.findViewById(R.id.home_appbarlayout) materialToolbar = view.findViewById(R.id.home_toolbar) queriesChipGroup = view.findViewById(R.id.queries) - searchSuggestions = view.findViewById(R.id.search_suggestions_scroll_view) - searchSuggestionsLinearLayout = view.findViewById(R.id.search_suggestions_linear_layout) - searchHistory = view.findViewById(R.id.search_history_scroll_view) - searchHistoryLinearLayout = view.findViewById(R.id.search_history_linear_layout) + queriesConstraint = view.findViewById(R.id.queries_constraint) homeFabs = view.findViewById(R.id.home_fabs) - downloadFabs = homeFabs!!.findViewById(R.id.download_selected_coordinator) - downloadAllFabCoordinator = homeFabs!!.findViewById(R.id.download_all_coordinator) + downloadSelectedFab = homeFabs!!.findViewById(R.id.download_selected_fab) + downloadAllFab = homeFabs!!.findViewById(R.id.download_all_fab) + clipboardFab = homeFabs!!.findViewById(R.id.copied_url_fab) runCatching { materialToolbar!!.title = ThemeUtil.getStyledAppName(requireContext()) } @@ -160,6 +159,17 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene recyclerView?.adapter = homeAdapter recyclerView?.enableFastScroll() + + searchSuggestionsAdapter = SearchSuggestionsAdapter( + this, + requireActivity() + ) + searchSuggestionsRecyclerView = view.findViewById(R.id.search_suggestions_recycler) + searchSuggestionsRecyclerView?.layoutManager = LinearLayoutManager(context) + searchSuggestionsRecyclerView?.adapter = searchSuggestionsAdapter + searchSuggestionsRecyclerView?.enableFastScroll() + + resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] resultViewModel.items.observe(requireActivity()) { kotlin.runCatching { @@ -167,9 +177,9 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene resultsList = it if(resultViewModel.repository.itemCount.value > 1 || resultViewModel.repository.itemCount.value == -1){ if (it.size > 1 && it[0].playlistTitle.isNotEmpty() && !loadingItems){ - downloadAllFabCoordinator!!.visibility = VISIBLE + downloadAllFab!!.visibility = VISIBLE }else{ - downloadAllFabCoordinator!!.visibility = GONE + downloadAllFab!!.visibility = GONE } }else if (resultViewModel.repository.itemCount.value == 1){ if (sharedPreferences!!.getBoolean("download_card", true)){ @@ -181,20 +191,17 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene } } }else{ - downloadAllFabCoordinator!!.visibility = GONE + downloadAllFab!!.visibility = GONE } quickLaunchSheet = true } } initMenu() - val downloadSelectedFab = downloadFabs!!.findViewById(R.id.download_selected_fab) - downloadSelectedFab.tag = "downloadSelected" - downloadSelectedFab.setOnClickListener(this) - val downloadAllFab = - downloadAllFabCoordinator!!.findViewById(R.id.download_all_fab) - downloadAllFab.tag = "downloadAll" - downloadAllFab.setOnClickListener(this) + downloadSelectedFab?.tag = "downloadSelected" + downloadSelectedFab?.setOnClickListener(this) + downloadAllFab?.tag = "downloadAll" + downloadAllFab?.setOnClickListener(this) if (arguments?.getString("url") != null){ val url = requireArguments().getString("url") @@ -247,9 +254,9 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene shimmerCards!!.stopShimmer() shimmerCards!!.visibility = GONE if (resultsList!!.size > 1 && resultsList!![0]!!.playlistTitle.isNotEmpty()){ - downloadAllFabCoordinator!!.visibility = VISIBLE + this@HomeFragment.downloadAllFab!!.visibility = VISIBLE }else{ - downloadAllFabCoordinator!!.visibility = GONE + this@HomeFragment.downloadAllFab!!.visibility = GONE } } } @@ -286,7 +293,18 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene } if (searchView?.currentTransitionState == SearchView.TransitionState.SHOWN){ - updateSearchViewItems(searchView?.editText?.text, linkYouCopied) + updateSearchViewItems(searchView?.editText?.text) + } + + requireView().post { + checkClipboard().apply { + clipboardFab?.isVisible = this != null + clipboardFab?.setOnClickListener { + searchView!!.setText(this) + clipboardFab?.isVisible = false + initSearch(searchView!!) + } + } } } @@ -339,53 +357,13 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene chipGroupDivider?.visibility = VISIBLE } - try{ - val clipboard = - requireContext().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager - val clip = clipboard.primaryClip!!.getItemAt(0).text - if (Patterns.WEB_URL.matcher(clip.toString()).matches()) { - linkYouCopied!!.visibility = VISIBLE - val textView = linkYouCopied!!.findViewById(R.id.suggestion_text) - textView.text = getString(R.string.link_you_copied) - textView.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_language, 0, 0, 0) - val mb = linkYouCopied!!.findViewById(R.id.set_search_query_button) - mb.setImageResource(R.drawable.ic_plus) - - mb.setOnClickListener { - val present = queriesChipGroup!!.children.firstOrNull { (it as Chip).text.toString() == clip.toString() } - if (present == null) { - val chip = layoutinflater!!.inflate(R.layout.input_chip, queriesChipGroup, false) as Chip - chip.text = clip.toString() - chip.chipBackgroundColor = ColorStateList.valueOf(MaterialColors.getColor(requireContext(), R.attr.colorSecondaryContainer, Color.BLACK)) - chip.setOnClickListener { - queriesChipGroup!!.removeView(chip) - } - queriesChipGroup!!.addView(chip) - } - if (queriesChipGroup!!.childCount == 0) queriesConstraint.visibility = GONE - else queriesConstraint.visibility = VISIBLE - searchView!!.editText.setText("") - linkYouCopied!!.visibility = GONE - } - - textView.setOnClickListener { - searchView!!.setText(clip.toString()) - initSearch(searchView!!) - } - }else{ - linkYouCopied!!.visibility = GONE - } - updateSearchViewItems(searchView!!.editText.text, linkYouCopied) - }catch (e: Exception){ - e.printStackTrace() - linkYouCopied!!.visibility = GONE - } + updateSearchViewItems(searchView!!.editText.text) } } searchView!!.editText.doAfterTextChanged { if (searchView!!.currentTransitionState != SearchView.TransitionState.SHOWN) return@doAfterTextChanged - updateSearchViewItems(it, linkYouCopied) + updateSearchViewItems(it) } searchView!!.editText.setOnTouchListener(OnTouchListener { _, event -> @@ -405,6 +383,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene chip.chipBackgroundColor = ColorStateList.valueOf(MaterialColors.getColor(requireContext(), R.attr.colorSecondaryContainer, Color.BLACK)) chip.setOnClickListener { queriesChipGroup!!.removeView(chip) + if (queriesChipGroup!!.childCount == 0) queriesConstraint.visibility = GONE } queriesChipGroup!!.addView(chip) } @@ -430,12 +409,12 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene resultViewModel.getTrending() selectedObjects = ArrayList() searchBar!!.setText("") - downloadAllFabCoordinator!!.visibility = GONE - downloadFabs!!.visibility = GONE + downloadAllFab!!.visibility = GONE + downloadSelectedFab!!.visibility = GONE } R.id.delete_search -> { resultViewModel.deleteAllSearchQueryHistory() - searchSuggestionsLinearLayout!!.removeAllViews() + searchSuggestionsAdapter?.submitList(listOf()) } } true @@ -451,26 +430,20 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene } @SuppressLint("InflateParams") - private fun updateSearchViewItems(searchQuery: Editable?, linkYouCopied: View?) = lifecycleScope.launch(Dispatchers.Main) { + private fun updateSearchViewItems(searchQuery: Editable?) = lifecycleScope.launch(Dispatchers.Main) { lifecycleScope.launch { - searchSuggestionsLinearLayout!!.visibility = GONE - searchHistoryLinearLayout!!.visibility = GONE - searchSuggestionsLinearLayout!!.post { - searchSuggestionsLinearLayout!!.removeAllViews() - } - searchHistoryLinearLayout!!.post { - searchHistoryLinearLayout!!.removeAllViews() - } - - linkYouCopied!!.visibility = GONE - if (searchView!!.editText.text.isEmpty()){ searchView!!.editText.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0) }else{ searchView!!.editText.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_plus, 0) } + + val combinedList = mutableListOf() + val history = withContext(Dispatchers.IO){ resultViewModel.getSearchHistory().map { it.query }.filter { it.contains(searchQuery!!) } + }.map { + SearchSuggestionItem(it, SearchSuggestionType.HISTORY) } val suggestions = if (sharedPreferences!!.getBoolean("search_suggestions", false)){ withContext(Dispatchers.IO){ @@ -478,66 +451,80 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene } }else{ emptyList() + }.map { + SearchSuggestionItem(it, SearchSuggestionType.SUGGESTION) } - history.forEach { s -> - val v = LayoutInflater.from(fragmentContext).inflate(R.layout.search_suggestion_item, null) - val textView = v.findViewById(R.id.suggestion_text) - textView.text = s - textView.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_restore, 0, 0, 0) - Handler(Looper.getMainLooper()).post { - searchHistoryLinearLayout!!.addView( - v - ) - } - textView.setOnClickListener { - searchView!!.setText(s) - initSearch(searchView!!) - } - textView.setOnLongClickListener { - val deleteDialog = MaterialAlertDialogBuilder(requireContext()) - deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + s + "\"!") - deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() } - deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int -> - searchHistoryLinearLayout!!.removeView(v) - resultViewModel.removeSearchQueryFromHistory(s) - } - deleteDialog.show() - true - } + combinedList.addAll(history) + combinedList.addAll(suggestions) - val mb = v.findViewById(R.id.set_search_query_button) - mb.setOnClickListener { - searchView!!.editText.setText(s) - searchView!!.editText.setSelection(searchView!!.editText.length()) - } - } - searchHistoryLinearLayout!!.isVisible = history.isNotEmpty() - if (linkYouCopied.findViewById(R.id.suggestion_text).text.isNotEmpty()){ - linkYouCopied.visibility = VISIBLE + val url = checkClipboard() + url?.apply { + combinedList.add(0, SearchSuggestionItem(this, SearchSuggestionType.CLIPBOARD)) } - suggestions.forEach { s -> - val v = LayoutInflater.from(fragmentContext) - .inflate(R.layout.search_suggestion_item, null) - val textView = v.findViewById(R.id.suggestion_text) - textView.text = s - Handler(Looper.getMainLooper()).post { - searchSuggestionsLinearLayout!!.addView( - v - ) - } - textView.setOnClickListener { - searchView!!.setText(s) - initSearch(searchView!!) - } - val mb = v.findViewById(R.id.set_search_query_button) - mb.setOnClickListener { - searchView!!.editText.setText(s) - searchView!!.editText.setSelection(searchView!!.editText.length()) - } - } - searchSuggestionsLinearLayout!!.isVisible = suggestions.isNotEmpty() + + searchSuggestionsAdapter?.submitList(combinedList) + +// history.forEach { s -> +// val v = LayoutInflater.from(fragmentContext).inflate(R.layout.search_suggestion_item, null) +// val textView = v.findViewById(R.id.suggestion_text) +// textView.text = s +// textView.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_restore, 0, 0, 0) +// Handler(Looper.getMainLooper()).post { +// searchHistoryLinearLayout!!.addView( +// v +// ) +// } +// textView.setOnClickListener { +// searchView!!.setText(s) +// initSearch(searchView!!) +// } +// textView.setOnLongClickListener { +// val deleteDialog = MaterialAlertDialogBuilder(requireContext()) +// deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + s + "\"!") +// deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() } +// deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int -> +// searchHistoryLinearLayout!!.removeView(v) +// resultViewModel.removeSearchQueryFromHistory(s) +// } +// deleteDialog.show() +// true +// } +// +// val mb = v.findViewById(R.id.set_search_query_button) +// mb.setOnClickListener { +// searchView!!.editText.setText(s) +// searchView!!.editText.setSelection(searchView!!.editText.length()) +// } +// } +// searchHistoryLinearLayout!!.isVisible = history.isNotEmpty() +// if (linkYouCopied.findViewById(R.id.suggestion_text).text.isNotEmpty()){ +// linkYouCopied.visibility = VISIBLE +// clipboardFab?.isVisible = true +// } +// +// suggestions.forEach { s -> +// val v = LayoutInflater.from(fragmentContext) +// .inflate(R.layout.search_suggestion_item, null) +// val textView = v.findViewById(R.id.suggestion_text) +// textView.text = s +// Handler(Looper.getMainLooper()).post { +// searchSuggestionsLinearLayout!!.addView( +// v +// ) +// } +// textView.setOnClickListener { +// searchView!!.setText(s) +// initSearch(searchView!!) +// } +// val mb = v.findViewById(R.id.set_search_query_button) +// mb.setOnClickListener { +// searchView!!.editText.setText(s) +// searchView!!.editText.setSelection(searchView!!.editText.length()) +// } +// } +// searchSuggestionsLinearLayout!!.isVisible = suggestions.isNotEmpty() if (Patterns.WEB_URL.matcher(searchView!!.editText.text).matches()){ providersChipGroup?.visibility = GONE @@ -637,6 +624,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene showSingleDownloadSheet(item!!, type!!) } + @SuppressLint("RestrictedApi") private fun showSingleDownloadSheet( resultItem: ResultItem, type: DownloadViewModel.Type @@ -807,6 +795,15 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene } + private fun checkClipboard(): String?{ + return kotlin.runCatching { + val clipboard = requireContext().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager + val clip = clipboard.primaryClip!!.getItemAt(0).text + val matchesURL = Patterns.WEB_URL.matcher(clip.toString()).matches() + return if (matchesURL) clip.toString() else null + }.getOrNull() + } + override fun onStop() { actionMode?.finish() super.onStop() @@ -816,4 +813,49 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene companion object { private const val TAG = "HomeFragment" } + + override fun onSearchSuggestionClick(text: String) { + clipboardFab?.isVisible = false + searchView!!.setText(text) + initSearch(searchView!!) + } + + override fun onSearchSuggestionAdd(text: String) { + val present = queriesChipGroup!!.children.firstOrNull { (it as Chip).text.toString() == text } + if (present == null) { + val chip = layoutinflater!!.inflate(R.layout.input_chip, queriesChipGroup, false) as Chip + chip.text = text + chip.chipBackgroundColor = ColorStateList.valueOf(MaterialColors.getColor(requireContext(), R.attr.colorSecondaryContainer, Color.BLACK)) + chip.setOnClickListener { + queriesChipGroup!!.removeView(chip) + if (queriesChipGroup!!.childCount == 0) queriesChipGroup!!.visibility = View.GONE + } + queriesChipGroup!!.addView(chip) + } + if (queriesChipGroup!!.childCount == 0) queriesConstraint!!.visibility = View.GONE + else queriesConstraint!!.visibility = View.VISIBLE + searchView!!.editText.setText("") + val clipBoardItem = searchSuggestionsRecyclerView?.layoutManager?.findViewByPosition(0) + clipBoardItem?.apply { + if ((this as ConstraintLayout).findViewById(R.id.suggestion_text).text == getString(R.string.link_you_copied)){ + searchSuggestionsAdapter?.notifyItemRemoved(0) + } + } + } + + override fun onSearchSuggestionLongClick(text: String, position: Int) { + val deleteDialog = MaterialAlertDialogBuilder(requireContext()) + deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + text + "\"!") + deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() } + deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int -> + resultViewModel.removeSearchQueryFromHistory(text) + searchSuggestionsAdapter?.notifyItemRemoved(position) + } + deleteDialog.show() + } + + override fun onSearchSuggestionAddToSearchBar(text: String) { + searchView!!.editText.setText(text) + searchView!!.editText.setSelection(searchView!!.editText.length()) + } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/adapter/HistoryAdapter.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/adapter/HistoryAdapter.kt index 560d7667..7811eae0 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/adapter/HistoryAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/adapter/HistoryAdapter.kt @@ -8,12 +8,13 @@ import android.graphics.ColorMatrix import android.graphics.ColorMatrixColorFilter import android.os.Handler import android.os.Looper -import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.ViewTreeObserver.OnGlobalLayoutListener import android.widget.ImageView import android.widget.TextView +import androidx.annotation.OptIn import androidx.core.content.ContextCompat import androidx.preference.PreferenceManager import androidx.recyclerview.widget.AsyncDifferConfig @@ -23,7 +24,11 @@ import androidx.recyclerview.widget.RecyclerView import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.database.models.HistoryItem import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel +import com.google.android.material.badge.BadgeDrawable +import com.google.android.material.badge.BadgeUtils +import com.google.android.material.badge.ExperimentalBadgeUtils import com.google.android.material.card.MaterialCardView +import com.google.android.material.color.MaterialColors import com.google.android.material.floatingactionbutton.FloatingActionButton import com.squareup.picasso.Picasso import java.io.File @@ -54,10 +59,28 @@ class HistoryAdapter(onItemClickListener: OnItemClickListener, activity: Activit } } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val cardView = LayoutInflater.from(parent.context) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HistoryAdapter.ViewHolder { + return if (viewType == 0){ + val cardView = LayoutInflater.from(parent.context) .inflate(R.layout.history_card, parent, false) - return ViewHolder(cardView, onItemClickListener) + 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) { @@ -82,7 +105,7 @@ class HistoryAdapter(onItemClickListener: OnItemClickListener, activity: Activit // TITLE ---------------------------------- val itemTitle = card.findViewById(R.id.downloads_title) - var title = item!!.title + var title = item!!.title.ifEmpty { item.url } if (title.length > 100) { title = title.substring(0, 40) + "..." } @@ -98,44 +121,38 @@ class HistoryAdapter(onItemClickListener: OnItemClickListener, activity: Activit // TIME DOWNLOADED ---------------------------------- val datetime = card.findViewById(R.id.downloads_info_time) - -// val relativeTime = DateUtils.getRelativeTimeSpanString( -// item.time * 1000L, -// System.currentTimeMillis(), -// DateUtils.MINUTE_IN_MILLIS -// ) datetime.text = SimpleDateFormat(android.text.format.DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(item.time * 1000L) // BUTTON ---------------------------------- val btn = card.findViewById(R.id.downloads_download_button_type) - var filePresent = true + var filesPresent = true //IS IN THE FILE SYSTEM? - val path = item.downloadPath - val file = File(path) - if (!file.exists() && path.isNotEmpty()) { - filePresent = false + 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 = ContextCompat.getColorStateList(activity, R.color.black) - btn.imageTintList = ContextCompat.getColorStateList(activity, R.color.white) + btn.backgroundTintList = MaterialColors.getColorStateList(activity, R.attr.colorSurface, ContextCompat.getColorStateList(activity, android.R.color.transparent)!!) + }else{ + thumbnail.alpha = 1f + btn.backgroundTintList = MaterialColors.getColorStateList(activity, R.attr.colorPrimaryContainer, ContextCompat.getColorStateList(activity, android.R.color.transparent)!!) } if (item.type == DownloadViewModel.Type.audio) { - if (filePresent) btn.setImageResource(R.drawable.ic_music_downloaded) else { + if (filesPresent) btn.setImageResource(R.drawable.ic_music_downloaded) else { btn.setImageResource(R.drawable.ic_music) } } else if (item.type == DownloadViewModel.Type.video) { - if (filePresent) btn.setImageResource(R.drawable.ic_video_downloaded) else btn.setImageResource(R.drawable.ic_video) + if (filesPresent) btn.setImageResource(R.drawable.ic_video_downloaded) else btn.setImageResource(R.drawable.ic_video) }else{ - if (filePresent) btn.setImageResource(R.drawable.ic_terminal) else btn.setImageResource(R.drawable.baseline_code_off_24) + 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 = filePresent + btn.isClickable = filesPresent if (checkedItems.contains(item.id)) { card.isChecked = true @@ -144,7 +161,7 @@ class HistoryAdapter(onItemClickListener: OnItemClickListener, activity: Activit card.isChecked = false card.strokeWidth = 0 } - val finalFilePresent = filePresent + val finalFilePresent = filesPresent card.setOnLongClickListener { checkCard(card, item.id) true diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/adapter/SearchSuggestionsAdapter.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/adapter/SearchSuggestionsAdapter.kt new file mode 100644 index 00000000..f18f746c --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/adapter/SearchSuggestionsAdapter.kt @@ -0,0 +1,126 @@ +package com.deniscerri.ytdlnis.ui.adapter + +import android.app.Activity +import android.content.SharedPreferences +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageButton +import android.widget.LinearLayout +import android.widget.TextView +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.ytdlnis.R +import com.deniscerri.ytdlnis.database.models.SearchSuggestionItem +import com.deniscerri.ytdlnis.database.models.SearchSuggestionType + + +class SearchSuggestionsAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter(AsyncDifferConfig.Builder( + DIFF_CALLBACK +).build()) { + private val onItemClickListener: OnItemClickListener + private val activity: Activity + private val sharedPreferences: SharedPreferences + + init { + this.onItemClickListener = onItemClickListener + this.activity = activity + sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity) + } + + class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) { + val linear: LinearLayout + + init { + linear = itemView.findViewById(R.id.linear) + } + } + + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchSuggestionsAdapter.ViewHolder { + val cardView = LayoutInflater.from(parent.context) + .inflate(R.layout.search_suggestion_item, parent, false) + return ViewHolder(cardView, onItemClickListener) + } + + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = getItem(position) ?: return + val linear = holder.linear + when (item.type){ + SearchSuggestionType.SUGGESTION -> { + val textView = linear.findViewById(R.id.suggestion_text) + textView.text = item.text + textView.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_search, 0, 0, 0) + + textView.setOnClickListener { + onItemClickListener.onSearchSuggestionClick(item.text) + } + textView.setOnLongClickListener { true } + val mb = linear.findViewById(R.id.set_search_query_button) + mb.setOnClickListener { + onItemClickListener.onSearchSuggestionAddToSearchBar(item.text) + } + } + SearchSuggestionType.HISTORY -> { + val textView = linear.findViewById(R.id.suggestion_text) + textView.text = item.text + textView.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_restore, 0, 0, 0) + textView.setOnClickListener { + onItemClickListener.onSearchSuggestionClick(item.text) + } + textView.setOnLongClickListener { + onItemClickListener.onSearchSuggestionLongClick(item.text, position) + true + } + + val mb = linear.findViewById(R.id.set_search_query_button) + mb.setOnClickListener { + onItemClickListener.onSearchSuggestionAddToSearchBar(item.text) + } + } + SearchSuggestionType.CLIPBOARD -> { + val textView = linear.findViewById(R.id.suggestion_text) + textView.text = activity.getString(R.string.link_you_copied) + textView.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_language, 0, 0, 0) + val mb = linear.findViewById(R.id.set_search_query_button) + mb.setImageResource(R.drawable.ic_plus) + + mb.setOnClickListener { + onItemClickListener.onSearchSuggestionAdd(item.text) + } + + textView.setOnClickListener { + onItemClickListener.onSearchSuggestionClick(item.text) + } + textView.setOnLongClickListener { true } + } + } + } + + interface OnItemClickListener { + fun onSearchSuggestionClick(text: String) + fun onSearchSuggestionAdd(text: String) + + fun onSearchSuggestionLongClick(text: String, position: Int) + + fun onSearchSuggestionAddToSearchBar(text: String) + + } + + companion object { + private val DIFF_CALLBACK: DiffUtil.ItemCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: SearchSuggestionItem, newItem: SearchSuggestionItem): Boolean { + return oldItem.text == newItem.text && oldItem.type == newItem.type + } + + override fun areContentsTheSame(oldItem: SearchSuggestionItem, newItem: SearchSuggestionItem): Boolean { + return oldItem.text == newItem.text && oldItem.type == newItem.type + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/ConfigureDownloadBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/ConfigureDownloadBottomSheetDialog.kt index aa6d3ce5..2ebce177 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/ConfigureDownloadBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/ConfigureDownloadBottomSheetDialog.kt @@ -161,7 +161,7 @@ class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private putString("last_used_download_type", listOf(Type.audio, Type.video, Type.command)[position].toString()) } - updateTitleAuthorWhenSwitching() + updateWhenSwitching() } } }) @@ -205,7 +205,7 @@ class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private } - private fun updateTitleAuthorWhenSwitching(){ + private fun updateWhenSwitching(){ val prevDownloadItem = getDownloadItem( if (viewPager2.currentItem == 1) 0 else 1 ) @@ -223,6 +223,7 @@ class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private 1 -> { val f = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment f.updateTitleAuthor(prevDownloadItem.title, prevDownloadItem.author) + f.updateSelectedAudioFormat(getDownloadItem(0).format) } else -> {} } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadAudioFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadAudioFragment.kt index c8788e8e..8507441f 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadAudioFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadAudioFragment.kt @@ -70,8 +70,8 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private ): View? { fragmentView = inflater.inflate(R.layout.fragment_download_audio, container, false) activity = getActivity() - downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java] - resultViewModel = ViewModelProvider(requireActivity())[ResultViewModel::class.java] + downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] + resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] infoUtil = InfoUtil(requireContext()) genericAudioFormats = infoUtil.getGenericAudioFormats(requireContext().resources) preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) @@ -295,7 +295,8 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private updateDataClicked = { CoroutineScope(Dispatchers.IO).launch { resultItem?.apply { - resultViewModel.updateItemData(this) + val rsVM = ViewModelProvider(requireActivity())[ResultViewModel::class.java] + rsVM.updateItemData(this) } } }, diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadBottomSheetDialog.kt index 867963b0..38f51ddc 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadBottomSheetDialog.kt @@ -9,29 +9,25 @@ import android.os.Build import android.os.Bundle import android.text.format.DateFormat import android.util.DisplayMetrics -import android.util.Log import android.util.Patterns import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.LinearLayout +import android.widget.TextView import android.widget.Toast import androidx.core.content.edit import androidx.core.os.bundleOf import androidx.core.view.isVisible -import androidx.fragment.app.commit import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope -import androidx.media3.common.Player.Command import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceManager import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.widget.ViewPager2 import com.afollestad.materialdialogs.utils.MDUtil.getStringArray import com.deniscerri.ytdlnis.R -import com.deniscerri.ytdlnis.database.DBManager -import com.deniscerri.ytdlnis.database.dao.CommandTemplateDao import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.ResultItem import com.deniscerri.ytdlnis.database.repository.DownloadRepository @@ -246,6 +242,9 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() { if (tab!!.position == 2 && commandTemplateNr == 0){ tabLayout.selectTab(tabLayout.getTabAt(1)) val s = Snackbar.make(view, getString(R.string.add_template_first), Snackbar.LENGTH_LONG) + val snackbarView: View = s.view + val snackTextView = snackbarView.findViewById(com.google.android.material.R.id.snackbar_text) as TextView + snackTextView.maxLines = 9999999 s.setAction(R.string.new_template){ UiUtil.showCommandTemplateCreationOrUpdatingSheet(item = null, context = requireActivity(), lifeCycle = this@DownloadBottomSheetDialog, commandTemplateViewModel = commandTemplateViewModel){ commandTemplateNr = 1 @@ -279,7 +278,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() { putString("last_used_download_type", listOf(Type.audio, Type.video, Type.command)[position].toString()) } - updateTitleAuthorWhenSwitching() + updateWhenSwitching() } } }) @@ -437,6 +436,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() { shimmerLoadingSubtitle.visibility = View.VISIBLE shimmerLoading.startShimmer() shimmerLoadingSubtitle.startShimmer() + updateItem.visibility = View.GONE }else{ title.visibility = View.VISIBLE subtitle.visibility = View.VISIBLE @@ -600,7 +600,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() { } } - private fun updateTitleAuthorWhenSwitching(){ + private fun updateWhenSwitching(){ val prevDownloadItem = getDownloadItem( if (viewPager2.currentItem == 1) 0 else 1 ) @@ -617,6 +617,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() { kotlin.runCatching { val f = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment f.updateTitleAuthor(prevDownloadItem.title, prevDownloadItem.author) + f.updateSelectedAudioFormat(getDownloadItem(0).format) } } 2 -> { diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadCommandFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadCommandFragment.kt index e040ab17..0dffa682 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadCommandFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadCommandFragment.kt @@ -12,6 +12,7 @@ import android.util.Patterns import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.inputmethod.InputMethodManager import android.widget.* import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity @@ -123,6 +124,13 @@ class DownloadCommandFragment(private val resultItem: ResultItem? = null, privat } }) + chosenCommandView.editText!!.setSelection(chosenCommandView.editText!!.text.length) + val imm = context?.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager + chosenCommandView.editText!!.postDelayed({ + chosenCommandView.editText!!.requestFocus() + imm.showSoftInput(chosenCommandView.editText, 0) + }, 300) + chosenCommandView.setEndIconOnClickListener { if(chosenCommandView.editText!!.text.isEmpty()){ val clipboard: ClipboardManager = @@ -252,8 +260,9 @@ class DownloadCommandFragment(private val resultItem: ResultItem? = null, privat shortcutClicked = { UiUtil.showShortcuts(requireActivity(), commandTemplateViewModel, itemSelected = { - chosenCommandView.editText!!.setText("${chosenCommandView.editText!!.text} $it") - downloadItem.format.format_note = chosenCommandView.editText!!.text.toString() + val selectionStart = chosenCommandView.editText!!.selectionStart + chosenCommandView.editText!!.text.insert(selectionStart, it) + chosenCommandView.editText!!.setSelection(selectionStart + it.length) preferences.edit().putString("lastCommandTemplateUsed", downloadItem.format.format_note).apply() }, itemRemoved = {removed -> diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadMultipleBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadMultipleBottomSheetDialog.kt index 3063d5a8..2e08eb03 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadMultipleBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadMultipleBottomSheetDialog.kt @@ -33,6 +33,7 @@ import androidx.work.Data import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager +import com.afollestad.materialdialogs.utils.MDUtil.getStringArray import com.deniscerri.ytdlnis.MainActivity import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.ui.adapter.ConfigureMultipleDownloadsAdapter @@ -136,7 +137,9 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure recyclerView.layoutManager = LinearLayoutManager(requireContext()) recyclerView.adapter = listAdapter recyclerView.enableFastScroll() - if(sharedPreferences.getBoolean("swipe_gestures", true)){ + + val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) + if (preferences.getStringSet("swipe_gesture", requireContext().getStringArray(R.array.swipe_gestures_values).toSet())!!.toList().contains("multipledownloadcard")){ val itemTouchHelper = ItemTouchHelper(simpleCallback) itemTouchHelper.attachToRecyclerView(recyclerView) } @@ -603,7 +606,8 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure items.forEach { it.downloadPath = result.data?.data.toString() } - Toast.makeText(requireContext(), "Changed every item's download path to: ${FileUtil.formatPath(result.data!!.data.toString())}", Toast.LENGTH_LONG).show() + val path = FileUtil.formatPath(result.data!!.data.toString()) + Snackbar.make(requireView(),getString(R.string.changed_path_for_everyone_to) + " " + path, Snackbar.LENGTH_LONG).show() } } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadVideoFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadVideoFragment.kt index e554cb52..35185717 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadVideoFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadVideoFragment.kt @@ -74,8 +74,8 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private ): View? { fragmentView = inflater.inflate(R.layout.fragment_download_video, container, false) activity = getActivity() - downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java] - resultViewModel = ViewModelProvider(requireActivity())[ResultViewModel::class.java] + downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] + resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] infoUtil = InfoUtil(requireContext()) genericVideoFormats = infoUtil.getGenericVideoFormats(requireContext().resources) preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) @@ -218,8 +218,9 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private downloadItem.videoPreferences.audioFormatIDs.clear() downloadItem.videoPreferences.audioFormatIDs.addAll(it) } - UiUtil.populateFormatCard(requireContext(), formatCard, item.first().format, item.first().audioFormats) - + UiUtil.populateFormatCard(requireContext(), formatCard, item.first().format, + if(downloadItem.videoPreferences.removeAudio) listOf() else item.first().audioFormats + ) } override fun onFormatsUpdated(allFormats: List>) { @@ -241,7 +242,9 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private val preferredAudioFormats = downloadViewModel.getPreferredAudioFormats(formats) downloadItem.format = preferredFormat downloadItem.allFormats = formats - UiUtil.populateFormatCard(requireContext(), formatCard, preferredFormat, formats.filter { preferredAudioFormats.contains(it.format_id) }) + UiUtil.populateFormatCard(requireContext(), formatCard, preferredFormat, + if(downloadItem.videoPreferences.removeAudio) listOf() else formats.filter { preferredAudioFormats.contains(it.format_id) } + ) } } @@ -315,7 +318,8 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private updateDataClicked = { CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) { resultItem?.apply { - resultViewModel.updateItemData(this) + val rsVM = ViewModelProvider(requireActivity())[ResultViewModel::class.java] + rsVM.updateItemData(this) } } }, @@ -330,6 +334,7 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private }, removeAudioClicked = { downloadItem.videoPreferences.removeAudio = it + UiUtil.populateFormatCard(requireContext(), formatCard, downloadItem.format, if (it) listOf() else downloadItem.allFormats.filter { downloadItem.videoPreferences.audioFormatIDs.contains(it.format_id) }) }, extraCommandsClicked = { val callback = object : ExtraCommandsListener { @@ -366,6 +371,16 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private onViewCreated(requireView(),savedInstanceState = state) } + @SuppressLint("RestrictedApi") + fun updateSelectedAudioFormat(format: Format){ + downloadItem.videoPreferences.audioFormatIDs.clear() + downloadItem.videoPreferences.audioFormatIDs.addAll(arrayListOf(format.format_id)) + val formatCard = requireView().findViewById(R.id.format_card_constraintLayout) + UiUtil.populateFormatCard(requireContext(), formatCard, downloadItem.format, + if(downloadItem.videoPreferences.removeAudio) listOf() else listOf(format) + ) + } + private var pathResultLauncher = registerForActivityResult( ActivityResultContracts.StartActivityForResult() ) { result -> diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/FormatSelectionBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/FormatSelectionBottomSheetDialog.kt index 4e7f77d4..ca4aaab9 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/FormatSelectionBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/FormatSelectionBottomSheetDialog.kt @@ -7,22 +7,31 @@ import android.os.Bundle import android.util.DisplayMetrics import android.view.LayoutInflater import android.view.View +import android.view.Window import android.widget.* +import androidx.core.view.children import androidx.core.view.forEach import androidx.core.view.isVisible +import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager import com.deniscerri.ytdlnis.R +import com.deniscerri.ytdlnis.database.DBManager import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.Format +import com.deniscerri.ytdlnis.database.repository.HistoryRepository +import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel.Type import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.UiUtil import com.facebook.shimmer.ShimmerFrameLayout import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.card.MaterialCardView +import com.google.android.material.chip.Chip +import com.google.android.material.chip.ChipGroup import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -35,10 +44,11 @@ import java.util.* class FormatSelectionBottomSheetDialog(private val items: List, private var formats: List>, private val listener: OnFormatClickListener) : BottomSheetDialogFragment() { private lateinit var behavior: BottomSheetBehavior private lateinit var infoUtil: InfoUtil + private lateinit var downloadViewModel: DownloadViewModel private lateinit var sharedPreferences: SharedPreferences private lateinit var formatCollection: MutableList> private lateinit var chosenFormats: List - private lateinit var selectedVideo : Format + private var selectedVideo : Format? = null private lateinit var selectedAudios : MutableList private lateinit var videoFormatList : LinearLayout @@ -48,15 +58,23 @@ class FormatSelectionBottomSheetDialog(private val items: List, p private lateinit var audioTitle : TextView private lateinit var sortBy : FormatSorting + private lateinit var filterBy : FormatCategory + private lateinit var filterBtn : Button private lateinit var continueInBackgroundSnackBar : Snackbar private lateinit var view: View private var updateFormatsJob: Job? = null + private var hasGenericFormats: Boolean = false + enum class FormatSorting { filesize, container, codec, id } + enum class FormatCategory { + ALL, SUGGESTED, SMALLEST, GENERIC + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) infoUtil = InfoUtil(requireActivity().applicationContext) @@ -64,6 +82,7 @@ class FormatSelectionBottomSheetDialog(private val items: List, p chosenFormats = listOf() selectedAudios = mutableListOf() sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) + downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] } @@ -74,6 +93,8 @@ class FormatSelectionBottomSheetDialog(private val items: List, p dialog.setContentView(view) sortBy = FormatSorting.valueOf(sharedPreferences.getString("format_order", "filesize")!!) + filterBy = FormatCategory.valueOf(sharedPreferences.getString("format_filter", "ALL")!!) + filterBtn = view.findViewById(R.id.format_filter) dialog.setOnShowListener { behavior = BottomSheetBehavior.from(view.parent as View) @@ -92,7 +113,9 @@ class FormatSelectionBottomSheetDialog(private val items: List, p okBtn = view.findViewById(R.id.format_ok) shimmers.visibility = View.GONE - val hasGenericFormats = formats.first().isEmpty() || formats.last().any { it.format_id == "best" || it.format_id == "ba" } + hasGenericFormats = formats.first().isEmpty() || formats.last().any { it.format_id == "best" || it.format_id == "ba" } + filterBtn.isVisible = !hasGenericFormats + if (items.size > 1){ if (!hasGenericFormats){ @@ -140,16 +163,18 @@ class FormatSelectionBottomSheetDialog(private val items: List, p chosenFormats = emptyList() - try { - refreshBtn.isEnabled = false - formatListLinearLayout.visibility = View.GONE - shimmers.visibility = View.VISIBLE - shimmers.startShimmer() - updateFormatsJob = launch(Dispatchers.IO) { + refreshBtn.isEnabled = false + formatListLinearLayout.visibility = View.GONE + shimmers.visibility = View.VISIBLE + shimmers.startShimmer() + updateFormatsJob = launch { + try{ //simple download if (items.size == 1) { kotlin.runCatching { - val res = infoUtil.getFormats(items.first()!!.url) + val res = withContext(Dispatchers.IO){ + infoUtil.getFormats(items.first()!!.url) + } res.filter { it.format_note != "storyboard" } chosenFormats = if (items.first()?.type == Type.audio) { res.filter { it.format_note.contains("audio", ignoreCase = true) } @@ -172,19 +197,21 @@ class FormatSelectionBottomSheetDialog(private val items: List, p } } - //list format filtering + //list format filtering }else{ var progress = "0/${items.size}" formatCollection.clear() withContext(Dispatchers.Main) { refreshBtn.text = progress } - infoUtil.getFormatsMultiple(items.map { it!!.url }) { - if (isActive) { - lifecycleScope.launch(Dispatchers.Main) { - formatCollection.add(it) - progress = "${formatCollection.size}/${items.size}" - refreshBtn.text = progress + withContext(Dispatchers.IO){ + infoUtil.getFormatsMultiple(items.map { it!!.url }) { + if (isActive) { + lifecycleScope.launch(Dispatchers.Main) { + formatCollection.add(it) + progress = "${formatCollection.size}/${items.size}" + refreshBtn.text = progress + } } } } @@ -218,32 +245,34 @@ class FormatSelectionBottomSheetDialog(private val items: List, p .sumOf { itt -> itt.filesize } } } + hasGenericFormats = false withContext(Dispatchers.Main){ shimmers.visibility = View.GONE shimmers.stopShimmer() + filterBtn.isVisible = true addFormatsToView() refreshBtn.visibility = View.GONE formatListLinearLayout.visibility = View.VISIBLE } - } - updateFormatsJob?.start() - }catch (e: Exception){ - runCatching { - refreshBtn.isEnabled = true - refreshBtn.text = getString(R.string.update_formats) - formatListLinearLayout.visibility = View.VISIBLE - shimmers.visibility = View.GONE - shimmers.stopShimmer() + }catch (e: Exception){ + withContext(Dispatchers.Main) { + refreshBtn.isEnabled = true + refreshBtn.text = getString(R.string.update_formats) + formatListLinearLayout.visibility = View.VISIBLE + shimmers.visibility = View.GONE + shimmers.stopShimmer() - e.printStackTrace() - Toast.makeText(context, getString(R.string.error_updating_formats), Toast.LENGTH_SHORT).show() + e.printStackTrace() + Toast.makeText(context, getString(R.string.error_updating_formats), Toast.LENGTH_SHORT).show() + } } } + updateFormatsJob?.start() } } okBtn.setOnClickListener { - if (!::selectedVideo.isInitialized) { + if (selectedVideo == null) { selectedVideo = chosenFormats.filter { it.vcodec.isNotBlank() && it.vcodec != "none" }.maxByOrNull { it.filesize }!! } @@ -254,21 +283,85 @@ class FormatSelectionBottomSheetDialog(private val items: List, p // if (sharedPreferences.getBoolean("update_formats", false) && refreshBtn.isVisible && items.size == 1){ // refreshBtn.performClick() // } + + initFilter() + } + + + private fun initFilter(){ + filterBtn.setOnClickListener { + val filterSheet = BottomSheetDialog(requireContext()) + filterSheet.requestWindowFeature(Window.FEATURE_NO_TITLE) + filterSheet.setContentView(R.layout.format_category_sheet) + + val all = filterSheet.findViewById(R.id.all) + val suggested = filterSheet.findViewById(R.id.suggested) + val smallest = filterSheet.findViewById(R.id.smallest) + val generic = filterSheet.findViewById(R.id.generic) + + val filterOptions = listOf(all!!, suggested!!,smallest!!, generic!!) + filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) } + when(filterBy) { + FormatCategory.ALL -> all.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0) + FormatCategory.SUGGESTED -> { + suggested.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0) + } + FormatCategory.SMALLEST -> { + smallest.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0) + } + FormatCategory.GENERIC -> { + generic.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0) + } + } + + all.setOnClickListener { + filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) } + filterBy = FormatCategory.ALL + all.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0) + addFormatsToView() + filterSheet.dismiss() + } + suggested.setOnClickListener { + filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) } + filterBy = FormatCategory.SUGGESTED + suggested.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0) + addFormatsToView() + filterSheet.dismiss() + } + smallest.setOnClickListener { + filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) } + filterBy = FormatCategory.SMALLEST + smallest.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0) + addFormatsToView() + filterSheet.dismiss() + } + generic.setOnClickListener { + filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) } + filterBy = FormatCategory.GENERIC + generic.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0) + addFormatsToView() + filterSheet.dismiss() + } + val displayMetrics = DisplayMetrics() + requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics) + filterSheet.behavior.peekHeight = displayMetrics.heightPixels + filterSheet.show() + } } private fun returnFormats(){ //simple video format selection if (items.size == 1){ - listener.onFormatClick(listOf(FormatTuple(selectedVideo, selectedAudios))) + listener.onFormatClick(listOf(FormatTuple(selectedVideo!!, selectedAudios.ifEmpty { listOf(downloadViewModel.getFormat(chosenFormats, Type.audio)) }))) }else{ //playlist format selection val selectedFormats = mutableListOf() formatCollection.forEach { - selectedFormats.add(it.first{ f -> f.format_id == selectedVideo.format_id}) + selectedFormats.add(it.first{ f -> f.format_id == selectedVideo!!.format_id}) } if (selectedFormats.isEmpty()) { items.forEach { _ -> - selectedFormats.add(selectedVideo) + selectedFormats.add(selectedVideo!!) } } listener.onFormatClick(selectedFormats.map { FormatTuple(it, selectedAudios) }) @@ -292,6 +385,53 @@ class FormatSelectionBottomSheetDialog(private val items: List, p FormatSorting.filesize -> chosenFormats } + //filter category + when(filterBy){ + FormatCategory.ALL -> {} + FormatCategory.SUGGESTED -> { + finalFormats = if (items.first()?.type == Type.audio){ + val req = downloadViewModel.getPreferredAudioRequirements() + finalFormats + .filter { f -> req.count{req -> req(f)} >= 1 } + .sortedByDescending { f -> req.count{req -> req(f)} } + .ifEmpty { listOf(downloadViewModel.getFormat(finalFormats, Type.audio)) } + }else{ + val req = downloadViewModel.getPreferredVideoRequirements() + req.addAll(downloadViewModel.getPreferredAudioRequirements()) + finalFormats + .filter { f -> req.count{req -> req(f)} >= 1 } + .sortedByDescending { f -> req.count{req -> req(f)} } + .ifEmpty { listOf(downloadViewModel.getFormat(finalFormats, Type.video)) } + } + } + FormatCategory.SMALLEST -> { + val tmpFormats = finalFormats + .asSequence() + .map { it.copy() } + .filter { it.filesize > 0 } + .onEach { + var tmp = it.format_note + //formats that end like 1080P + if (tmp.endsWith("060")){ + tmp = tmp.removeSuffix("60") + } + tmp = tmp.removeSuffix("p") + //formats that are written like 1920X1080 + val split = tmp.split("x") + if (split.size > 1){ + tmp = split[1] + } + it.format_note = tmp + } + .groupBy { it.format_note } + .map { it.value.minBy { it2 -> it2.filesize } }.toList() + finalFormats = finalFormats.filter { tmpFormats.map { it2 -> it2.format_id }.contains(it.format_id) } + } + FormatCategory.GENERIC -> { + finalFormats = listOf() + } + } + val canMultiSelectAudio = items.first()?.type == Type.video && finalFormats.find { it.format_note.contains("audio", ignoreCase = true) } != null videoFormatList.removeAllViews() audioFormatList.removeAllViews() @@ -315,6 +455,10 @@ class FormatSelectionBottomSheetDialog(private val items: List, p } } + + videoFormatList.removeAllViews() + audioFormatList.removeAllViews() + if (finalFormats.isEmpty()){ finalFormats = if (items.first()?.type == Type.audio){ infoUtil.getGenericAudioFormats(requireContext().resources) @@ -328,6 +472,8 @@ class FormatSelectionBottomSheetDialog(private val items: List, p val formatItem = LayoutInflater.from(context).inflate(R.layout.format_item, null) formatItem.tag = "${format.format_id}${format.format_note}" UiUtil.populateFormatCard(requireContext(), formatItem as MaterialCardView, format, null) + if (selectedVideo == format) formatItem.isChecked = true + if (selectedAudios.any { it == format }) formatItem.isChecked = true formatItem.setOnClickListener{ clickedformat -> //if the context is behind a video or playlist, allow the ability to multiselect audio formats if (canMultiSelectAudio){ @@ -380,6 +526,31 @@ class FormatSelectionBottomSheetDialog(private val items: List, p }else{ videoFormatList.addView(formatItem) } + + + } + + if (items.first()?.type == Type.video){ + selectedVideo = null + run breaking@{ + videoFormatList.children.forEach { + val card = it as MaterialCardView + if (card.isChecked){ + selectedVideo = finalFormats.first { format -> "${format.format_id}${format.format_note}" == card.tag } + return@breaking + } + } + } + }else{ + selectedAudios = mutableListOf() + run breaking@{ + audioFormatList.children.forEach { + val card = it as MaterialCardView + if (card.isChecked){ + selectedAudios.add(finalFormats.first { format -> "${format.format_id}${format.format_note}" == card.tag }) + } + } + } } } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloads/HistoryFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloads/HistoryFragment.kt index 9d846116..38c931e9 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloads/HistoryFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloads/HistoryFragment.kt @@ -1,5 +1,6 @@ package com.deniscerri.ytdlnis.ui.downloads +import android.annotation.SuppressLint import android.app.Activity import android.content.Context import android.content.DialogInterface @@ -103,6 +104,7 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{ return fragmentView } + @SuppressLint("RestrictedApi") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -519,7 +521,7 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{ true } R.id.share -> { - UiUtil.shareFileIntent(requireContext(), selectedObjects!!.map { it.downloadPath }) + UiUtil.shareFileIntent(requireContext(), selectedObjects!!.map { it.downloadPath }.flatten()) clearCheckedItems() actionMode?.finish() true @@ -633,7 +635,7 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{ override fun onButtonClick(itemID: Long, isPresent: Boolean) { if (isPresent){ - UiUtil.shareFileIntent(requireContext(), listOf(historyList!!.first { it!!.id == itemID }!!.downloadPath)) + UiUtil.shareFileIntent(requireContext(), historyList!!.first { it!!.id == itemID }!!.downloadPath) } } companion object { diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/downloadLogs/DownloadLogFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/downloadLogs/DownloadLogFragment.kt index c0f300ae..cdee6b64 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/downloadLogs/DownloadLogFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/downloadLogs/DownloadLogFragment.kt @@ -4,42 +4,32 @@ import android.annotation.SuppressLint import android.app.ActionBar import android.content.ClipboardManager import android.content.Context.CLIPBOARD_SERVICE -import android.graphics.Color import android.os.Bundle -import android.text.method.LinkMovementMethod -import android.text.method.MovementMethod -import android.text.method.ScrollingMovementMethod -import android.util.Log import android.view.LayoutInflater import android.view.MenuItem -import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.widget.HorizontalScrollView import android.widget.LinearLayout import android.widget.ScrollView import android.widget.TextView -import androidx.core.view.children import androidx.core.view.get +import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope import com.deniscerri.ytdlnis.MainActivity import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.database.viewmodel.LogViewModel import com.deniscerri.ytdlnis.util.Extensions.enableTextHighlight +import com.deniscerri.ytdlnis.util.Extensions.setCustomTextSize import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.bottomappbar.BottomAppBar import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton +import com.google.android.material.slider.Slider import com.google.android.material.snackbar.Snackbar -import com.neo.highlight.core.Highlight -import com.neo.highlight.util.listener.HighlightTextWatcher -import com.neo.highlight.util.scheme.ColorScheme import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.util.regex.Pattern class DownloadLogFragment : Fragment() { @@ -105,7 +95,7 @@ class DownloadLogFragment : Fragment() { content.isFocusable = true content.enableTextHighlight() - + val slider = view.findViewById(R.id.textsize_seekbar) bottomAppBar?.setOnMenuItemClickListener { m: MenuItem -> when(m.itemId){ R.id.wrap -> { @@ -140,10 +130,23 @@ class DownloadLogFragment : Fragment() { m.isVisible = false contentScrollView.fullScroll(View.FOCUS_DOWN) } + + R.id.text_size -> { + slider!!.isVisible = !slider.isVisible + } } true } + slider?.apply { + this.valueFrom = 0f + this.valueTo = 10f + this.value = 2f + this.addOnChangeListener { slider, value, fromUser -> + content.setCustomTextSize(value + 13f) + } + } + contentScrollView.setOnScrollChangeListener { view, sx, sy, osx, osy -> if (sy < osy){ bottomAppBar?.menu?.get(1)?.isVisible = contentScrollView.canScrollVertically(1) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/MainSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/MainSettingsFragment.kt index a72e38f8..f681d731 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/MainSettingsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/MainSettingsFragment.kt @@ -25,6 +25,7 @@ import androidx.preference.PreferenceManager import androidx.work.WorkInfo import androidx.work.WorkManager import com.deniscerri.ytdlnis.BuildConfig +import com.deniscerri.ytdlnis.MainActivity import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.database.models.CommandTemplate import com.deniscerri.ytdlnis.database.models.CookieItem @@ -201,7 +202,7 @@ class MainSettingsFragment : PreferenceFragmentCompat() { saveFile.writeText(GsonBuilder().setPrettyPrinting().create().toJson(json)) val s = Snackbar.make(requireView(), getString(R.string.backup_created_successfully), Snackbar.LENGTH_LONG) s.setAction(R.string.Open_File){ - UiUtil.openFileIntent(requireContext(), saveFile.absolutePath) + UiUtil.openFileIntent(requireActivity(), saveFile.absolutePath) } s.show() } @@ -600,10 +601,11 @@ class MainSettingsFragment : PreferenceFragmentCompat() { builder.setPositiveButton( getString(R.string.restart) ) { _: DialogInterface?, _: Int -> - val intent: Intent? = requireContext().packageManager - .getLaunchIntentForPackage(requireContext().packageName) - intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - startActivity(intent!!) + val intent = Intent(requireContext(), MainActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(intent) + requireActivity().finishAffinity() if(json.has("settings")){ AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(preferences.getString("app_language", "en"))) } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/terminal/TerminalFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/terminal/TerminalFragment.kt index 81e2c92c..7b3284a0 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/terminal/TerminalFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/terminal/TerminalFragment.kt @@ -8,7 +8,6 @@ import android.content.Context.INPUT_METHOD_SERVICE import android.content.Intent import android.content.SharedPreferences import android.os.Bundle -import android.text.method.ScrollingMovementMethod import android.util.DisplayMetrics import android.view.LayoutInflater import android.view.MenuItem @@ -23,6 +22,7 @@ import android.widget.TextView import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.core.view.get +import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider @@ -38,15 +38,16 @@ import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.NotificationUtil import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.util.Extensions.enableTextHighlight +import com.deniscerri.ytdlnis.util.Extensions.setCustomTextSize import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.bottomappbar.BottomAppBar import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton +import com.google.android.material.slider.Slider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.util.concurrent.Executor import kotlin.properties.Delegates @@ -236,6 +237,8 @@ class TerminalFragment : Fragment() { topAppBar?.menu?.get(0)?.isVisible = false topAppBar?.menu?.get(1)?.isVisible = true topAppBar?.menu?.get(2)?.isVisible = true + topAppBar?.menu?.get(3)?.isVisible = true + val slider = requireActivity().findViewById(R.id.textsize_seekbar) topAppBar?.setOnMenuItemClickListener { m: MenuItem -> when(m.itemId){ R.id.wrap -> { @@ -264,9 +267,23 @@ class TerminalFragment : Fragment() { clipboard.setText(output?.text) } } + + R.id.text_size -> { + slider?.isVisible = !slider.isVisible + } } true } + + slider?.apply { + this.valueFrom = 0f + this.valueTo = 10f + this.value = 2f + this.addOnChangeListener { slider, value, fromUser -> + output?.setCustomTextSize(value + 13f) + input?.setCustomTextSize(value + 13f) + } + } } private fun hideCancelFab() { kotlin.runCatching { @@ -276,7 +293,7 @@ class TerminalFragment : Fragment() { } private fun showCancelFab() { kotlin.runCatching { - fab!!.text = getString(R.string.cancel_download) + fab!!.text = getString(R.string.cancel_task) fab!!.setIconResource(R.drawable.ic_cancel) } } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/util/Extensions.kt b/app/src/main/java/com/deniscerri/ytdlnis/util/Extensions.kt index b45f0fef..93e2b972 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/util/Extensions.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/util/Extensions.kt @@ -11,7 +11,9 @@ import android.graphics.drawable.shapes.OvalShape import android.media.MediaMetadataRetriever import android.media.MediaMetadataRetriever.METADATA_KEY_DURATION import android.net.Uri +import android.os.Build import android.util.DisplayMetrics +import android.util.TypedValue import android.view.MotionEvent import android.view.View import android.view.ViewGroup @@ -25,6 +27,7 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.whenStarted import androidx.lifecycle.withStarted import androidx.recyclerview.widget.RecyclerView +import com.deniscerri.ytdlnis.database.models.CommandTemplate import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.neo.highlight.core.Highlight @@ -32,6 +35,7 @@ import com.neo.highlight.util.listener.HighlightTextWatcher import com.neo.highlight.util.scheme.ColorScheme import kotlinx.coroutines.launch import me.zhanghai.android.fastscroll.FastScrollerBuilder +import org.json.JSONObject import java.io.File import java.util.regex.Pattern @@ -158,4 +162,29 @@ object Extensions { } } } + + fun JSONObject.getIntByAny(vararg tags:String): Int { + tags.forEach { + runCatching { + return this.getInt(it) + } + } + return -1 + } + + fun JSONObject.getStringByAny(vararg tags:String): String { + tags.forEach { + runCatching { + val tmp = this.getString(it) + if (tmp != "null") return tmp + } + } + + return "" + } + + fun TextView.setCustomTextSize(newSize: Float){ + this.setTextSize(TypedValue.COMPLEX_UNIT_SP, newSize) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/util/FileUtil.kt b/app/src/main/java/com/deniscerri/ytdlnis/util/FileUtil.kt index 957a0345..6c2a0522 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/util/FileUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/util/FileUtil.kt @@ -253,7 +253,7 @@ object FileUtil { MediaScannerConnection.scanFile(context, arrayOf(it), null, null) } } - return paths + return paths.sortedBy { File(it).lastModified() } }catch (e: Exception){ e.printStackTrace() } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/util/InfoUtil.kt b/app/src/main/java/com/deniscerri/ytdlnis/util/InfoUtil.kt index a2eec5bf..305b62cb 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/util/InfoUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/util/InfoUtil.kt @@ -18,6 +18,8 @@ import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.Format import com.deniscerri.ytdlnis.database.models.ResultItem import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel +import com.deniscerri.ytdlnis.util.Extensions.getIntByAny +import com.deniscerri.ytdlnis.util.Extensions.getStringByAny import com.google.gson.Gson import com.google.gson.reflect.TypeToken import com.yausername.youtubedl_android.YoutubeDL @@ -617,27 +619,13 @@ class InfoUtil(private val context: Context) { for (result in results) { if (result.isNullOrBlank()) continue val jsonObject = JSONObject(result) - val title = if (jsonObject.has("title")) { - if (jsonObject.getString("title") == "[Private video]") continue - jsonObject.getString("title") - } else { - jsonObject.getString("webpage_url_basename") - } - var author: String = if (jsonObject.has("uploader")) jsonObject.getString("uploader") else "" - if (author.isEmpty() || author == "null"){ - author = if (jsonObject.has("channel")) jsonObject.getString("channel") else "" - if (author.isEmpty() || author == "null"){ - author = if (jsonObject.has("playlist_uploader")) jsonObject.getString("playlist_uploader") else "" - if (author.isEmpty() || author == "null"){ - author = if (jsonObject.has("uploader_id")) jsonObject.getString("uploader_id") else "" - } - } - } - var duration = "" - runCatching { - if (jsonObject.has("duration")) { - duration = formatIntegerDuration(jsonObject.getInt("duration"), Locale.US) - } + val title = jsonObject.getStringByAny("track", "alt_title", "title", "webpage_url_basename") + if (title == "[Private video]") continue + + val author = jsonObject.getStringByAny("uploader", "channel", "playlist_uploader", "uploader_id") + var duration = jsonObject.getIntByAny("duration").toString() + if (duration != "-1"){ + duration = formatIntegerDuration(jsonObject.getInt("duration"), Locale.US) } var thumb: String? = "" @@ -649,16 +637,15 @@ class InfoUtil(private val context: Context) { thumb = thumbs.getJSONObject(thumbs.length() - 1).getString("url") } } - val website = jsonObject.getString(listOf("ie_key", "extractor_key", "extractor").first { jsonObject.has(it) }) - var playlistTitle: String? = "" + val website = jsonObject.getStringByAny("ie_key", "extractor_key", "extractor") + var playlistTitle = jsonObject.getStringByAny("playlist_title") var playlistURL: String? = "" var playlistIndex: Int? = null - if (jsonObject.has("playlist_title")) playlistTitle = jsonObject.getString("playlist_title") - if(playlistTitle?.removeSurrounding("\"").equals(query)) playlistTitle = "" + if(playlistTitle.removeSurrounding("\"") == query) playlistTitle = "" - if (playlistTitle?.isNotBlank() == true){ + if (playlistTitle.isNotBlank()){ playlistURL = query kotlin.runCatching { playlistIndex = jsonObject.getInt("playlist_index") } } @@ -703,7 +690,7 @@ class InfoUtil(private val context: Context) { duration, thumb!!, website, - playlistTitle!!, + playlistTitle, formats, urls, chapters, @@ -788,22 +775,12 @@ class InfoUtil(private val context: Context) { val youtubeDLResponse = YoutubeDL.getInstance().execute(request) val jsonObject = JSONObject(youtubeDLResponse.out) - var author: String = if (jsonObject.has("uploader")) jsonObject.getString("uploader") else "" - if (author.isEmpty() || author == "null"){ - author = if (jsonObject.has("channel")) jsonObject.getString("channel") else "" - if (author.isEmpty() || author == "null"){ - author = if (jsonObject.has("playlist_uploader")) jsonObject.getString("playlist_uploader") else "" - if (author.isEmpty() || author == "null"){ - author = if (jsonObject.has("uploader_id")) jsonObject.getString("uploader_id") else "" - } - } - } + val title = jsonObject.getStringByAny("track", "alt_title", "title", "webpage_url_basename") + val author = jsonObject.getStringByAny("uploader", "channel", "playlist_uploader", "uploader_id") - var duration = "" - runCatching { - if (jsonObject.has("duration")) { - duration = formatIntegerDuration(jsonObject.getInt("duration"), Locale.US) - } + var duration = jsonObject.getIntByAny("duration").toString() + if (duration != "-1"){ + duration = formatIntegerDuration(jsonObject.getInt("duration"), Locale.US) } var thumb: String? = "" @@ -820,11 +797,7 @@ class InfoUtil(private val context: Context) { return ResultItem( 0, url, - if (isPlaylist){ - "" - }else{ - jsonObject.getString("title") - }, + if (isPlaylist) "" else title, author, duration, thumb!!, @@ -986,8 +959,7 @@ class InfoUtil(private val context: Context) { YoutubeDLRequest(downloadItem.url) }else{ YoutubeDLRequest(downloadItem.playlistURL!!).apply { - addOption("--playlist-start", downloadItem.playlistIndex!!) - addOption("--playlist-end", downloadItem.playlistIndex!!) + addOption("-I", "${downloadItem.playlistIndex!!}:${downloadItem.playlistIndex}") } } @@ -1117,7 +1089,7 @@ class InfoUtil(private val context: Context) { "%(section_title&{} |)s$filenameTemplate" } if (downloadItem.downloadSections.split(";").size > 1){ - filenameTemplate = "%(autonumber)d. %(section_start>%H:%M:%S)s $filenameTemplate" + filenameTemplate = "%(autonumber)d. %(section_start>%H∶%M∶%S)s $filenameTemplate" } } @@ -1145,11 +1117,11 @@ class InfoUtil(private val context: Context) { } if (downloadItem.playlistTitle.isNotBlank()){ - request.addOption("--parse-metadata","${downloadItem.playlistTitle}:%(playlist)s") + request.addCommands(listOf("--replace-in-metadata", "playlist", ".+", downloadItem.playlistTitle)) runCatching { - request.addOption("--parse-metadata", - downloadItem.playlistIndex.toString() + ":%(playlist_index)s" - ) + if (downloadItem.playlistIndex != null){ + request.addOption("--parse-metadata", downloadItem.playlistIndex.toString() + ":%(playlist_index)s") + } } } @@ -1209,6 +1181,7 @@ class InfoUtil(private val context: Context) { request.addOption("--embed-metadata") request.addOption("--parse-metadata", "%(release_year,release_date>%Y,upload_date>%Y)s:%(meta_date)s") + request.addOption("--parse-metadata", "video:%(uploader)s:%(album_artist)s") if (downloadItem.playlistTitle.isNotEmpty()) { request.addOption("--parse-metadata", "%(album,playlist,title)s:%(meta_album)s") @@ -1279,6 +1252,10 @@ class InfoUtil(private val context: Context) { val f = StringBuilder() + val preferredCodec = sharedPreferences.getString("video_codec", "") + val vCodecPrefIndex = context.getStringArray(R.array.video_codec_values).indexOf(preferredCodec) + val vCodecPref = context.getStringArray(R.array.video_codec_values_ytdlp)[vCodecPrefIndex] + val defaultFormats = context.resources.getStringArray(R.array.video_formats_values) val usingGenericFormat = defaultFormats.contains(videoF) || downloadItem.allFormats.isEmpty() || downloadItem.allFormats == getGenericVideoFormats(context.resources) if (!usingGenericFormat){ @@ -1333,9 +1310,7 @@ class InfoUtil(private val context: Context) { request.addOption("--audio-multistreams") } - val preferredCodec = sharedPreferences.getString("video_codec", "") - val vCodecPrefIndex = context.getStringArray(R.array.video_codec_values).indexOf(preferredCodec) - val vCodecPref = context.getStringArray(R.array.video_codec_values_ytdlp)[vCodecPrefIndex] + preferredFormatIDs.forEach { v -> preferredAudioFormatIDs.forEach { a -> val aa = if (a.isNotBlank()) "+$a" else "" @@ -1345,13 +1320,21 @@ class InfoUtil(private val context: Context) { if (!downloadItem.videoPreferences.removeAudio){ //build format with audio with preferred language if (preferredAudioLanguage.isNotBlank()){ - val al = "$v+ba[language^=$preferredAudioLanguage]/" + val al = if (v == "wv"){ + "$v+wa[language^=$preferredAudioLanguage]/" + }else{ + "$v+ba[language^=$preferredAudioLanguage]/" + } if (!f.contains(al)) f.append(al) } //build format with best audio if (!f.contains("$v+ba/") && !f.contains("wa")) f.append("$v+ba/") //build format with standalone video - f.append("$v/") + if (v == "wv"){ + f.append("w/") + }else{ + if (v != "bv") f.append("$v/") + } } } @@ -1360,15 +1343,15 @@ class InfoUtil(private val context: Context) { f.append("b") } - StringBuilder().apply { - if (vCodecPref.isNotBlank()) append(",vcodec:$vCodecPref") - if (aCodecPref.isNotBlank()) append(",acodec:$aCodecPref") - if (cont.isNotBlank()) append(",ext:$cont") - if (this.isNotBlank()){ - request.addOption("-S", "+hasaud$this") - } - } + } + StringBuilder().apply { + if (vCodecPref.isNotBlank()) append(",vcodec:$vCodecPref") + if (aCodecPref.isNotBlank()) append(",acodec:$aCodecPref") + if (cont.isNotBlank()) append(",ext:$cont") + if (this.isNotBlank()){ + request.addOption("-S", "+hasaud$this") + } } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/util/NotificationUtil.kt b/app/src/main/java/com/deniscerri/ytdlnis/util/NotificationUtil.kt index 18c4f3cd..dfae540c 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/util/NotificationUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/util/NotificationUtil.kt @@ -550,7 +550,7 @@ class NotificationUtil(var context: Context) { } fun showQueriesFinished() { - val notificationBuilder = getBuilder(DOWNLOAD_FINISHED_CHANNEL_ID) + val notificationBuilder = getBuilder(DOWNLOAD_MISC_CHANNEL_ID) val openMultipleDownloads = NavDeepLinkBuilder(context) .setGraph(R.navigation.nav_graph) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/util/UiUtil.kt b/app/src/main/java/com/deniscerri/ytdlnis/util/UiUtil.kt index 4fb0866f..19d4d3bd 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/util/UiUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/util/UiUtil.kt @@ -18,6 +18,7 @@ import android.util.Log import android.view.Gravity import android.view.View import android.view.ViewGroup +import android.view.ViewTreeObserver import android.view.Window import android.view.inputmethod.InputMethodManager import android.widget.Button @@ -26,17 +27,15 @@ import android.widget.EditText import android.widget.LinearLayout import android.widget.TextView import android.widget.Toast +import androidx.annotation.OptIn import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.content.res.AppCompatResources -import androidx.compose.ui.text.font.Typeface -import androidx.compose.ui.unit.dp +import androidx.compose.ui.text.toUpperCase import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.FileProvider -import androidx.core.content.res.ResourcesCompat import androidx.core.view.children import androidx.core.view.isVisible -import androidx.core.view.updateLayoutParams import androidx.core.widget.doOnTextChanged import androidx.documentfile.provider.DocumentFile import androidx.fragment.app.FragmentManager @@ -57,6 +56,9 @@ import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel import com.deniscerri.ytdlnis.ui.downloadcard.ConfigureDownloadBottomSheetDialog import com.deniscerri.ytdlnis.ui.downloadcard.VideoCutListener import com.deniscerri.ytdlnis.util.Extensions.enableTextHighlight +import com.google.android.material.badge.BadgeDrawable +import com.google.android.material.badge.BadgeUtils +import com.google.android.material.badge.ExperimentalBadgeUtils import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.button.MaterialButton @@ -81,6 +83,7 @@ import kotlinx.coroutines.withContext import java.io.File import java.text.SimpleDateFormat import java.util.Calendar +import java.util.Date import java.util.Locale @@ -380,6 +383,46 @@ object UiUtil { } } + private fun openMultipleFilesIntent(context: Activity, path: List){ + val bottomSheet = BottomSheetDialog(context) + bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE) + bottomSheet.setContentView(R.layout.filepathlist) + + val list = bottomSheet.findViewById(R.id.filepath_list) + + list?.apply { + path.forEach {path -> + val file = File(path) + val item = context.layoutInflater.inflate(R.layout.filepath_card, list, false) + item.apply { + findViewById(R.id.file_name).text = file.nameWithoutExtension + findViewById(R.id.filesize).text = FileUtil.convertFileSize(file.length()) + findViewById(R.id.extension).text = file.extension.uppercase() + if (!file.exists()){ + isEnabled = false + alpha = 0.7f + } + isEnabled = file.exists() + setOnClickListener { + openFileIntent(context, path) + bottomSheet.dismiss() + } + } + list.addView(item) + + } + } + + bottomSheet.show() + val displayMetrics = DisplayMetrics() + context.windowManager.defaultDisplay.getMetrics(displayMetrics) + bottomSheet.behavior.peekHeight = displayMetrics.heightPixels + bottomSheet.window!!.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + } + fun shareFileIntent(context: Context, paths: List){ val uris : ArrayList = arrayListOf() paths.runCatching { @@ -479,14 +522,16 @@ object UiUtil { bottomSheet.setContentView(R.layout.history_item_details_bottom_sheet) bottomSheet.findViewById(R.id.bottom_sheet_title)?.apply { text = item.title.ifEmpty { "`${context.getString(R.string.defaultValue)}`" } - setOnClickListener{ + setOnLongClickListener{ showFullTextDialog(context, text.toString(), context.getString(R.string.title)) + true } } bottomSheet.findViewById(R.id.bottom_sheet_author)?.apply { text = item.author.ifEmpty { "`${context.getString(R.string.defaultValue)}`" } - setOnClickListener{ + setOnLongClickListener{ showFullTextDialog(context, text.toString(), context.getString(R.string.author)) + true } } @@ -682,8 +727,25 @@ object UiUtil { btn?.setImageResource(typeImageResource) if (isPresent){ - btn?.setOnClickListener { - shareFileIntent(context, listOf(item.downloadPath)) + btn?.apply { + if (item.downloadPath.size > 1){ + viewTreeObserver.addOnGlobalLayoutListener(object : + ViewTreeObserver.OnGlobalLayoutListener { + @OptIn(ExperimentalBadgeUtils::class) override fun onGlobalLayout() { + val badgeDrawable = BadgeDrawable.create(context) + badgeDrawable.number = item.downloadPath.size + //Important to change the position of the Badge + badgeDrawable.horizontalOffset = 25 + badgeDrawable.verticalOffset = 25 + BadgeUtils.attachBadgeDrawable(badgeDrawable, btn, null) + btn.viewTreeObserver.removeOnGlobalLayoutListener(this) + } + }) + } + + setOnClickListener { + shareFileIntent(context, item.downloadPath) + } } } @@ -693,7 +755,7 @@ object UiUtil { val codec = bottomSheet.findViewById(R.id.codec) val fileSize = bottomSheet.findViewById(R.id.file_size) val command = bottomSheet.findViewById(R.id.command) - val file = File(item.downloadPath) + val file = File(item.downloadPath.first()) val calendar = Calendar.getInstance() calendar.timeInMillis = item.time * 1000L @@ -708,7 +770,7 @@ object UiUtil { formatNote?.isVisible = false } - if (item.format.container != "") { + if (item.format.container != "" && item.downloadPath.size == 1) { container!!.text = if (file.exists()) file.extension.uppercase() else item.format.container.uppercase() container.setChipIconResource(typeImageResource) }else { @@ -723,7 +785,7 @@ object UiUtil { } else { item.format.acodec.uppercase() } - if (codecText == "" || codecText == "none"){ + if (codecText == "" || codecText == "none" || item.downloadPath.size > 1){ codec!!.visibility = View.GONE }else{ codec!!.visibility = View.VISIBLE @@ -731,7 +793,7 @@ object UiUtil { } val fileSizeReadable = FileUtil.convertFileSize(if (file.exists()) file.length() else item.format.filesize) - if (fileSizeReadable == "?") fileSize!!.visibility = View.GONE + if (fileSizeReadable == "?" || item.downloadPath.size > 1) fileSize!!.visibility = View.GONE else fileSize!!.text = fileSizeReadable command?.setOnClickListener { @@ -761,7 +823,11 @@ object UiUtil { val openFile = bottomSheet.findViewById