From 56979d78d80ee3a5ceb18019fdb464a56a598dbf Mon Sep 17 00:00:00 2001 From: deniscerri <64997243+deniscerri@users.noreply.github.com> Date: Tue, 3 Feb 2026 22:54:32 +0100 Subject: [PATCH] wipp --- .../deniscerri/ytdl/core/RuntimeManager.kt | 8 +- .../deniscerri/ytdl/core/plugins/Aria2c.kt | 11 + .../deniscerri/ytdl/core/plugins/FFmpeg.kt | 12 + .../deniscerri/ytdl/core/plugins/NodeJS.kt | 11 + .../ytdl/core/plugins/PluginBase.kt | 226 ++++++++++++++++++ .../deniscerri/ytdl/core/plugins/Python.kt | 11 + .../deniscerri/ytdl/core/runtimes/Aria2c.kt | 10 - .../ytdl/core/runtimes/BaseRuntime.kt | 190 --------------- .../deniscerri/ytdl/core/runtimes/FFmpeg.kt | 10 - .../deniscerri/ytdl/core/runtimes/NodeJS.kt | 10 - .../deniscerri/ytdl/core/runtimes/Python.kt | 10 - .../ytdl/database/models/PluginItem.kt | 10 + .../ytdl/ui/adapter/PluginsAdapter.kt | 84 +++++++ .../more/settings/updating/PluginsFragment.kt | 126 ++++++++++ .../updating/UpdateSettingsFragment.kt | 27 ++- app/src/main/res/layout/fragment_plugins.xml | 26 ++ app/src/main/res/layout/plugin_item.xml | 107 +++++++++ .../layout/plugin_releases_bottom_sheet.xml | 111 +++++++++ app/src/main/res/values/strings.xml | 3 +- app/src/main/res/xml/updating_preferences.xml | 10 +- 20 files changed, 762 insertions(+), 251 deletions(-) create mode 100644 app/src/main/java/com/deniscerri/ytdl/core/plugins/Aria2c.kt create mode 100644 app/src/main/java/com/deniscerri/ytdl/core/plugins/FFmpeg.kt create mode 100644 app/src/main/java/com/deniscerri/ytdl/core/plugins/NodeJS.kt create mode 100644 app/src/main/java/com/deniscerri/ytdl/core/plugins/PluginBase.kt create mode 100644 app/src/main/java/com/deniscerri/ytdl/core/plugins/Python.kt delete mode 100644 app/src/main/java/com/deniscerri/ytdl/core/runtimes/Aria2c.kt delete mode 100644 app/src/main/java/com/deniscerri/ytdl/core/runtimes/BaseRuntime.kt delete mode 100644 app/src/main/java/com/deniscerri/ytdl/core/runtimes/FFmpeg.kt delete mode 100644 app/src/main/java/com/deniscerri/ytdl/core/runtimes/NodeJS.kt delete mode 100644 app/src/main/java/com/deniscerri/ytdl/core/runtimes/Python.kt create mode 100644 app/src/main/java/com/deniscerri/ytdl/database/models/PluginItem.kt create mode 100644 app/src/main/java/com/deniscerri/ytdl/ui/adapter/PluginsAdapter.kt create mode 100644 app/src/main/java/com/deniscerri/ytdl/ui/more/settings/updating/PluginsFragment.kt create mode 100644 app/src/main/res/layout/fragment_plugins.xml create mode 100644 app/src/main/res/layout/plugin_item.xml create mode 100644 app/src/main/res/layout/plugin_releases_bottom_sheet.xml diff --git a/app/src/main/java/com/deniscerri/ytdl/core/RuntimeManager.kt b/app/src/main/java/com/deniscerri/ytdl/core/RuntimeManager.kt index 2e8e0e3a..1a0238a2 100644 --- a/app/src/main/java/com/deniscerri/ytdl/core/RuntimeManager.kt +++ b/app/src/main/java/com/deniscerri/ytdl/core/RuntimeManager.kt @@ -7,10 +7,10 @@ import com.deniscerri.ytdl.core.models.ExecuteException import com.deniscerri.ytdl.core.models.ExecuteResponse import com.deniscerri.ytdl.core.models.RuntimeLocation import com.deniscerri.ytdl.core.models.YTDLRequest -import com.deniscerri.ytdl.core.runtimes.Aria2c -import com.deniscerri.ytdl.core.runtimes.FFmpeg -import com.deniscerri.ytdl.core.runtimes.NodeJS -import com.deniscerri.ytdl.core.runtimes.Python +import com.deniscerri.ytdl.core.plugins.Aria2c +import com.deniscerri.ytdl.core.plugins.FFmpeg +import com.deniscerri.ytdl.core.plugins.NodeJS +import com.deniscerri.ytdl.core.plugins.Python import com.deniscerri.ytdl.core.stream.StreamGobbler import com.deniscerri.ytdl.core.stream.StreamProcessExtractor import org.apache.commons.io.FileUtils diff --git a/app/src/main/java/com/deniscerri/ytdl/core/plugins/Aria2c.kt b/app/src/main/java/com/deniscerri/ytdl/core/plugins/Aria2c.kt new file mode 100644 index 00000000..253dece1 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/core/plugins/Aria2c.kt @@ -0,0 +1,11 @@ +package com.deniscerri.ytdl.core.plugins + +object Aria2c : PluginBase() { + override val pluginName: String get() = "aria2c" + override val bundledZipName: String get() = "libaria2c.zip.so" + override val bundledVersion: String get() = "v1.37.0 [BUNDLED]" + override val manifestURL: String get() = "" + + @JvmStatic + fun getInstance() = this +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/core/plugins/FFmpeg.kt b/app/src/main/java/com/deniscerri/ytdl/core/plugins/FFmpeg.kt new file mode 100644 index 00000000..519ac578 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/core/plugins/FFmpeg.kt @@ -0,0 +1,12 @@ +package com.deniscerri.ytdl.core.plugins + +object FFmpeg : PluginBase() { + override val pluginName: String get() = "ffmpeg" + override val bundledZipName: String get() = "libffmpeg.zip.so" + override val bundledVersion: String get() = "v7.1.1 [BUNDLED]" + override val manifestURL: String get() = "" + + + @JvmStatic + fun getInstance() = this +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/core/plugins/NodeJS.kt b/app/src/main/java/com/deniscerri/ytdl/core/plugins/NodeJS.kt new file mode 100644 index 00000000..9f153b70 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/core/plugins/NodeJS.kt @@ -0,0 +1,11 @@ +package com.deniscerri.ytdl.core.plugins + +object NodeJS : PluginBase() { + override val pluginName: String get() = "node" + override val bundledZipName: String get() = "libnode.zip.so" + override val bundledVersion: String get() = "v25.3.0 [BUNDLED]" + override val manifestURL: String get() = "" + + @JvmStatic + fun getInstance() = this +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/core/plugins/PluginBase.kt b/app/src/main/java/com/deniscerri/ytdl/core/plugins/PluginBase.kt new file mode 100644 index 00000000..350746a5 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/core/plugins/PluginBase.kt @@ -0,0 +1,226 @@ +package com.deniscerri.ytdl.core.plugins + +import android.content.Context +import android.os.Build +import androidx.core.content.edit +import androidx.documentfile.provider.DocumentFile +import androidx.preference.PreferenceManager +import com.anggrayudi.storage.file.openInputStream +import com.deniscerri.ytdl.core.RuntimeManager +import com.deniscerri.ytdl.core.ZipUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import okhttp3.OkHttpClient +import okhttp3.Request +import org.apache.commons.io.FileUtils +import java.io.File + +abstract class PluginBase { + protected abstract val pluginName: String // e.g., "ffmpeg" + protected abstract val bundledZipName: String // e.g., "libffmpeg.zip.so" + protected abstract val bundledVersion: String // e.g., "v7.1" + protected abstract val githubRepositoryPackageURL: String // github repository package url + + @Serializable + data class PluginRelease( + @SerialName("name") + val version: String, + @SerialName("package_html_url") + val downloadUrl: String, + @SerialName("created_at") + val createdAt: String, + var isInstalled: Boolean + ) + + // Preferences Keys + private val installedKey get() = "${pluginName}_installed" + private val versionKey get() = "${pluginName}_version" + private val bundledVerKey get() = "${pluginName}_bundled_ver" + + lateinit var currentVersion : String + + companion object { + val sharedClient: OkHttpClient by lazy { + OkHttpClient.Builder() + .build() + } + } + + fun init(context: Context) { + val baseDir = File(context.noBackupFilesDir, RuntimeManager.BASENAME) + val packageDir = File(baseDir, "packages/$pluginName") + + if (!isDownloaded(context)) { + initBundled(context, packageDir) + } + + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + currentVersion = if (prefs.getBoolean(installedKey, false)) { + prefs.getString(versionKey, "unknown") ?: "unknown" + } else { + bundledVersion + } + } + + private fun initBundled(context: Context, targetDir: File) { + val bundledZip = File(context.applicationInfo.nativeLibraryDir, bundledZipName) + if (!bundledZip.exists()) return + + val currentSize = bundledZip.length().toString() + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + + if (!targetDir.exists() || prefs.getString(bundledVerKey, "") != currentSize) { + FileUtils.deleteQuietly(targetDir) + targetDir.mkdirs() + try { + ZipUtils.unzip(bundledZip, targetDir) + prefs.edit(commit = true) { putString(bundledVerKey, currentSize) } + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + private fun getRuntimeDir(context: Context) : File { + return File(context.noBackupFilesDir, "runtimes/$pluginName") + } + + suspend fun downloadRelease(context: Context, release: PluginRelease, onProgress: (Int) -> Unit) : File? { + val runtimeDir = getRuntimeDir(context) + FileUtils.deleteQuietly(runtimeDir) + runtimeDir.mkdirs() + + return withContext(Dispatchers.IO) { + try { + val tempZipFile = File(context.cacheDir, "${pluginName}_tmp.zip") + + //download + val request = Request.Builder().url(release.downloadUrl).build() + val response = sharedClient.newCall(request).execute() + if (!response.isSuccessful) return@withContext null + + val body = response.body + val totalBytes = body.contentLength() + var bytesDownloaded = 0L + + body.byteStream().use { inputStream -> + tempZipFile.outputStream().use { outputStream -> + val buffer = ByteArray(8192) // 8KB buffer + var bytesRead: Int + + while (inputStream.read(buffer).also { bytesRead = it } != -1) { + outputStream.write(buffer, 0, bytesRead) + bytesDownloaded += bytesRead + + // Calculate percentage + val progress = ((bytesDownloaded * 100) / totalBytes).toInt() + onProgress(progress) + } + } + } + + tempZipFile + } catch (e: Exception) { + null + } + } + } + + fun installFromZip(context: Context, zipFile: DocumentFile, versionTag: String? = null) : Result { + return kotlin.runCatching { + val runtimeDir = getRuntimeDir(context) + // 3. Unzip the main Bundle (contains libnode.so and libnode.zip.so) + val inputStream = zipFile.openInputStream(context) + ZipUtils.unzip(inputStream, runtimeDir) + inputStream?.close() + // 4. Handle the Bootstrap Zip (Double Unzip) + // Look for any .zip.so file in the extracted directory + runtimeDir.listFiles()?.forEach { file -> + if (file.name.endsWith(".zip.so")) { + ZipUtils.unzip(file, runtimeDir) + file.delete() // Remove the internal zip to save space + } + } + // 5. Global Permission Fix + // Scan for all files in any 'bin' folder and make them executable + applyExecutablePermissions(runtimeDir) + // 6. Save installation state + val version = versionTag ?: "IMPORTED" + saveState(context, version) + + if (versionTag != null) { + zipFile.delete() + } + + Result.success(version) + }.getOrElse { + Result.failure(it) + } + } + + private fun applyExecutablePermissions(file: File) { + if (file.isDirectory) { + // Check if this folder is a 'bin' folder + if (file.name == "bin") { + file.listFiles()?.forEach { it.setExecutable(true, false) } + } + // Recurse into subdirectories + file.listFiles()?.forEach { applyExecutablePermissions(it) } + } else if (file.name == "libnode.so") { + // Specifically ensure our renamed main binary is executable + file.setExecutable(true, false) + } + } + + private fun saveState(context: Context, version: String) { + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + prefs.edit(commit = true) { + putBoolean(installedKey, true) + putString(versionKey, version) + } + } + + suspend fun getReleases(context: Context) : List { + val request = Request.Builder() + .url(githubRepositoryPackageURL) + .header("Accept", "application/vnd.github+json") + .build() + + return withContext(Dispatchers.IO) { + try { + val response = sharedClient.newCall(request).execute() + val jsonString = response.body.string() + val json = Json { ignoreUnknownKeys = true } + if (response.isSuccessful && jsonString.isNotEmpty()) { + val supportedArch = getArchSuffix() + + json.decodeFromString>(jsonString) + .filter { it.version.contains(supportedArch) } + .onEach { + it.isInstalled = currentVersion == it.version + } + } else { + emptyList() + } + } catch (e: Exception) { + listOf() + } + } + } + + fun isDownloaded(context: Context) = + PreferenceManager.getDefaultSharedPreferences(context).getBoolean(installedKey, false) + + fun getArchSuffix(): String { + val abi = Build.SUPPORTED_ABIS[0] + return when { + abi.startsWith("arm64") -> "arm64-v8a" + abi.startsWith("armeabi") -> "armeabi-v7a" + abi.startsWith("x86_64") -> "x86_64" + else -> "arm64-v8a" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/core/plugins/Python.kt b/app/src/main/java/com/deniscerri/ytdl/core/plugins/Python.kt new file mode 100644 index 00000000..0e4e39f9 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/core/plugins/Python.kt @@ -0,0 +1,11 @@ +package com.deniscerri.ytdl.core.plugins + +object Python : PluginBase() { + override val pluginName: String get() = "python" + override val bundledZipName: String get() = "libpython.zip.so" + override val bundledVersion: String get() = "v3.12.11 [BUNDLED]" + override val manifestURL: String get() = "" + + @JvmStatic + fun getInstance() = this +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/core/runtimes/Aria2c.kt b/app/src/main/java/com/deniscerri/ytdl/core/runtimes/Aria2c.kt deleted file mode 100644 index dde516d1..00000000 --- a/app/src/main/java/com/deniscerri/ytdl/core/runtimes/Aria2c.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.deniscerri.ytdl.core.runtimes - -object Aria2c : BaseRuntime() { - override val runtimeName: String get() = "aria2c" - override val bundledZipName: String get() = "libaria2c.zip.so" - override val manifestURL: String get() = "" - - @JvmStatic - fun getInstance() = this -} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/core/runtimes/BaseRuntime.kt b/app/src/main/java/com/deniscerri/ytdl/core/runtimes/BaseRuntime.kt deleted file mode 100644 index 3d2923e1..00000000 --- a/app/src/main/java/com/deniscerri/ytdl/core/runtimes/BaseRuntime.kt +++ /dev/null @@ -1,190 +0,0 @@ -package com.deniscerri.ytdl.core.runtimes - -import android.content.Context -import android.os.Build -import androidx.core.content.edit -import androidx.preference.PreferenceManager -import com.deniscerri.ytdl.core.RuntimeManager -import com.deniscerri.ytdl.core.ZipUtils -import org.apache.commons.io.FileUtils -import org.json.JSONObject -import java.io.File -import java.net.HttpURLConnection -import java.net.URL - -abstract class BaseRuntime { - protected abstract val runtimeName: String // e.g., "ffmpeg" - protected abstract val bundledZipName: String // e.g., "libffmpeg.zip.so" - protected abstract val manifestURL: String // RUNTIME ZIP SOURCE API URL - - data class RuntimeUpdateInfo(val version: String, val downloadUrl: String) - - // Preferences Keys - private val installedKey get() = "${runtimeName}_installed" - private val versionKey get() = "${runtimeName}_version" - private val bundledVerKey get() = "${runtimeName}_bundled_ver" - - fun init(context: Context) { - val baseDir = File(context.noBackupFilesDir, RuntimeManager.BASENAME) - val packageDir = File(baseDir, "packages/$runtimeName") - - if (!isDownloaded(context)) { - initBundled(context, packageDir) - } - } - - private fun initBundled(context: Context, targetDir: File) { - val bundledZip = File(context.applicationInfo.nativeLibraryDir, bundledZipName) - if (!bundledZip.exists()) return - - val currentSize = bundledZip.length().toString() - val prefs = PreferenceManager.getDefaultSharedPreferences(context) - - if (!targetDir.exists() || prefs.getString(bundledVerKey, "") != currentSize) { - FileUtils.deleteQuietly(targetDir) - targetDir.mkdirs() - try { - ZipUtils.unzip(bundledZip, targetDir) - prefs.edit(commit = true) { putString(bundledVerKey, currentSize) } - } catch (e: Exception) { - e.printStackTrace() - } - } - } - - fun downloadAndInstall(context: Context, zipUrl: String, versionTag: String, onProgress: (Long, Long) -> Unit) : Boolean { - // 1. Clean up old installation - val runtimeDir = File(context.noBackupFilesDir, "runtimes/$runtimeName") - FileUtils.deleteQuietly(runtimeDir) - runtimeDir.mkdirs() - - var success = false - - val tempZip = File(context.cacheDir, "${runtimeName}_bundle_tmp.zip") - try { - // 2. Download the bundle (handles GitHub 302 redirects) - downloadWithRedirects(zipUrl, tempZip, onProgress) - // 3. Unzip the main Bundle (contains libnode.so and libnode.zip.so) - ZipUtils.unzip(tempZip, runtimeDir) - // 4. Handle the Bootstrap Zip (Double Unzip) - // Look for any .zip.so file in the extracted directory - runtimeDir.listFiles()?.forEach { file -> - if (file.name.endsWith(".zip.so")) { - ZipUtils.unzip(file, runtimeDir) - file.delete() // Remove the internal zip to save space - } - } - // 5. Global Permission Fix - // Scan for all files in any 'bin' folder and make them executable - applyExecutablePermissions(runtimeDir) - // 6. Save installation state - saveState(context, versionTag) - success = true - } catch (ex: Exception) { - success = false - } - if (tempZip.exists()) tempZip.delete() - return success - } - - private fun downloadWithRedirects(url: String, dest: File, onProgress: (Long, Long) -> Unit) { - var currentUrl = url - var connection: HttpURLConnection - var redirectCount = 0 - - while (true) { - connection = URL(currentUrl).openConnection() as HttpURLConnection - connection.instanceFollowRedirects = true - - val status = connection.responseCode - if (status == HttpURLConnection.HTTP_MOVED_TEMP || - status == HttpURLConnection.HTTP_MOVED_PERM || - status == HttpURLConnection.HTTP_SEE_OTHER) { - - currentUrl = connection.getHeaderField("Location") - redirectCount++ - if (redirectCount > 5) throw Exception("Too many redirects") - continue - } - break - } - - val fileSize = connection.contentLength.toLong() - var bytesCopied = 0L - val buffer = ByteArray(8192) - - connection.inputStream.use { input -> - dest.outputStream().use { output -> - var bytes = input.read(buffer) - while (bytes >= 0) { - output.write(buffer, 0, bytes) - bytesCopied += bytes - // Trigger the callback - onProgress?.invoke(bytesCopied, fileSize) - bytes = input.read(buffer) - } - } - } - } - - private fun applyExecutablePermissions(file: File) { - if (file.isDirectory) { - // Check if this folder is a 'bin' folder - if (file.name == "bin") { - file.listFiles()?.forEach { it.setExecutable(true, false) } - } - // Recurse into subdirectories - file.listFiles()?.forEach { applyExecutablePermissions(it) } - } else if (file.name == "libnode.so") { - // Specifically ensure our renamed main binary is executable - file.setExecutable(true, false) - } - } - - fun getVersion(context: Context): String { - val prefs = PreferenceManager.getDefaultSharedPreferences(context) - return if (prefs.getBoolean(installedKey, false)) { - prefs.getString(versionKey, "unknown") ?: "unknown" - } else { - "PRE-BUNDLED" - } - } - - private fun saveState(context: Context, version: String) { - val prefs = PreferenceManager.getDefaultSharedPreferences(context) - prefs.edit(commit = true) { - putBoolean(installedKey, true) - putString(versionKey, version) - } - } - - open fun checkForUpdates(context: Context): RuntimeUpdateInfo? { - return try { - val response = URL(manifestURL).readText() - val json = JSONObject(response) - val latestVersion = json.getString("version") - if (latestVersion != getVersion(context)) { - val urls = json.getJSONObject("urls") - val downloadUrl = urls.getString(getArchSuffix()) - RuntimeUpdateInfo(latestVersion, downloadUrl) - } else { - null - } - } catch (e: Exception) { - null - } - } - - fun isDownloaded(context: Context) = - PreferenceManager.getDefaultSharedPreferences(context).getBoolean(installedKey, false) - - fun getArchSuffix(): String { - val abi = Build.SUPPORTED_ABIS[0] - return when { - abi.startsWith("arm64") -> "arm64-v8a" - abi.startsWith("armeabi") -> "armeabi-v7a" - abi.startsWith("x86_64") -> "x86_64" - else -> "arm64-v8a" - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/core/runtimes/FFmpeg.kt b/app/src/main/java/com/deniscerri/ytdl/core/runtimes/FFmpeg.kt deleted file mode 100644 index 3038416e..00000000 --- a/app/src/main/java/com/deniscerri/ytdl/core/runtimes/FFmpeg.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.deniscerri.ytdl.core.runtimes - -object FFmpeg : BaseRuntime() { - override val runtimeName: String get() = "ffmpeg" - override val bundledZipName: String get() = "libffmpeg.zip.so" - override val manifestURL: String get() = "" - - @JvmStatic - fun getInstance() = this -} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/core/runtimes/NodeJS.kt b/app/src/main/java/com/deniscerri/ytdl/core/runtimes/NodeJS.kt deleted file mode 100644 index f71b0301..00000000 --- a/app/src/main/java/com/deniscerri/ytdl/core/runtimes/NodeJS.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.deniscerri.ytdl.core.runtimes - -object NodeJS : BaseRuntime() { - override val runtimeName: String get() = "node" - override val bundledZipName: String get() = "libnode.zip.so" - override val manifestURL: String get() = "" - - @JvmStatic - fun getInstance() = this -} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/core/runtimes/Python.kt b/app/src/main/java/com/deniscerri/ytdl/core/runtimes/Python.kt deleted file mode 100644 index 9f77a2fb..00000000 --- a/app/src/main/java/com/deniscerri/ytdl/core/runtimes/Python.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.deniscerri.ytdl.core.runtimes - -object Python : BaseRuntime() { - override val runtimeName: String get() = "python" - override val bundledZipName: String get() = "libpython.zip.so" - override val manifestURL: String get() = "" - - @JvmStatic - fun getInstance() = this -} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/database/models/PluginItem.kt b/app/src/main/java/com/deniscerri/ytdl/database/models/PluginItem.kt new file mode 100644 index 00000000..f8807ca2 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/database/models/PluginItem.kt @@ -0,0 +1,10 @@ +package com.deniscerri.ytdl.database.models + +import com.deniscerri.ytdl.core.plugins.PluginBase + + +data class PluginItem( + val title: String, + var version: String, + val instance: PluginBase +) \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/PluginsAdapter.kt b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/PluginsAdapter.kt new file mode 100644 index 00000000..0911ce0d --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/PluginsAdapter.kt @@ -0,0 +1,84 @@ +package com.deniscerri.ytdl.ui.adapter + +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.text.format.DateFormat +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.AsyncDifferConfig +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.deniscerri.ytdl.R +import com.deniscerri.ytdl.core.plugins.PluginBase +import com.deniscerri.ytdl.database.models.DownloadItem +import com.deniscerri.ytdl.database.models.GithubRelease +import com.deniscerri.ytdl.database.models.PluginItem +import com.google.android.material.chip.Chip +import com.google.android.material.chip.ChipGroup +import io.noties.markwon.AbstractMarkwonPlugin +import io.noties.markwon.Markwon +import io.noties.markwon.MarkwonConfiguration +import java.text.SimpleDateFormat +import java.util.Locale + +class PluginsAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter(AsyncDifferConfig.Builder( + DIFF_CALLBACK +).build()) { + private val activity: Activity + private val onItemClickListener: OnItemClickListener + + init { + this.onItemClickListener = onItemClickListener + this.activity = activity + } + + class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + var layoutParams: LinearLayout.LayoutParams + init { + layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT + ) + layoutParams.setMargins(10, 10, 10, 0) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val cardView = LayoutInflater.from(parent.context) + .inflate(R.layout.plugin_item, parent, false) + return ViewHolder(cardView) + } + + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val it = getItem(position) ?: return + val card = holder.itemView + + card.findViewById(R.id.title).text = it.title + card.findViewById(R.id.version).text = it.version + card.setOnClickListener { cl -> + onItemClickListener.onCardClick(it) + } + } + interface OnItemClickListener { + fun onCardClick(item: PluginItem) + } + + companion object { + private val DIFF_CALLBACK: DiffUtil.ItemCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: PluginItem, newItem: PluginItem): Boolean { + return oldItem.title == newItem.title + } + + override fun areContentsTheSame(oldItem: PluginItem, newItem: PluginItem): Boolean { + return oldItem.version == newItem.version + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/updating/PluginsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/updating/PluginsFragment.kt new file mode 100644 index 00000000..6e82398b --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/updating/PluginsFragment.kt @@ -0,0 +1,126 @@ +package com.deniscerri.ytdl.ui.more.settings.updating + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Intent +import android.content.SharedPreferences +import android.os.Bundle +import android.util.DisplayMetrics +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.Window +import android.widget.Button +import android.widget.TextView +import androidx.activity.result.contract.ActivityResultContracts +import androidx.documentfile.provider.DocumentFile +import androidx.fragment.app.Fragment +import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.deniscerri.ytdl.R +import com.deniscerri.ytdl.core.plugins.NodeJS +import com.deniscerri.ytdl.database.models.PluginItem +import com.deniscerri.ytdl.ui.adapter.PluginsAdapter +import com.deniscerri.ytdl.ui.more.settings.SettingsActivity +import com.deniscerri.ytdl.util.Extensions.enableFastScroll +import com.google.android.material.bottomsheet.BottomSheetDialog + + +class PluginsFragment : Fragment(), PluginsAdapter.OnItemClickListener { + private lateinit var recyclerView: RecyclerView + private lateinit var listAdapter: PluginsAdapter + private lateinit var settingsActivity: SettingsActivity + private lateinit var preferences: SharedPreferences + + private var tmpItem: PluginItem? = null + private var plugins: List = mutableListOf() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + settingsActivity = activity as SettingsActivity + settingsActivity.changeTopAppbarTitle(getString(R.string.plugins)) + preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) + return inflater.inflate(R.layout.fragment_plugins, container, false) + } + + @SuppressLint("RestrictedApi") + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + listAdapter = PluginsAdapter(this, settingsActivity) + recyclerView = view.findViewById(R.id.recycler_view) + recyclerView.layoutManager = LinearLayoutManager(context) + recyclerView.adapter = listAdapter + recyclerView.enableFastScroll() + + val nodeJSInstance = NodeJS.getInstance() + + plugins = listOf( + PluginItem("NodeJS", nodeJSInstance.currentVersion, nodeJSInstance) + ) + listAdapter.submitList(plugins) + } + + override fun onCardClick(item: PluginItem) { + + val bottomSheet = BottomSheetDialog(requireContext()) + bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE) + bottomSheet.setContentView(R.layout.plugin_releases_bottom_sheet) + + bottomSheet.findViewById(R.id.bottom_sheet_title)?.text = item.title + bottomSheet.findViewById