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