pull/1112/head
deniscerri 5 months ago
parent cb19cb4685
commit 56979d78d8
No known key found for this signature in database
GPG Key ID: 95C43D517D830350

@ -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

@ -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
}

@ -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
}

@ -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
}

@ -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<String> {
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<PluginRelease> {
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<List<PluginRelease>>(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"
}
}
}

@ -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
}

@ -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
}

@ -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"
}
}
}

@ -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
}

@ -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
}

@ -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
}

@ -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
)

@ -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<PluginItem?, PluginsAdapter.ViewHolder>(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<TextView>(R.id.title).text = it.title
card.findViewById<TextView>(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<PluginItem> = object : DiffUtil.ItemCallback<PluginItem>() {
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
}
}
}
}

@ -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<PluginItem> = 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<TextView>(R.id.bottom_sheet_title)?.text = item.title
bottomSheet.findViewById<Button>(R.id.bottomsheet_import_zip)?.setOnClickListener {
tmpItem = item
bottomSheet.dismiss()
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/zip"
}
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
importPluginZipLauncher.launch(intent)
}
//TODO SHOW RELEASES LIST
bottomSheet.show()
val displayMetrics = DisplayMetrics()
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
bottomSheet.behavior.peekHeight = displayMetrics.heightPixels
bottomSheet.window!!.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
private var importPluginZipLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let {
activity?.contentResolver?.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
tmpItem?.let { item ->
DocumentFile.fromSingleUri(requireContext(), it)?.apply {
val result = item.instance.installFromZip(requireContext(), this)
if (result.isSuccess) {
val idx = plugins.indexOfFirst { it2 -> it2.title == item.title }
plugins[idx].version = result.getOrNull() ?: ""
listAdapter.submitList(plugins)
}
}
}
}
}
}
}

@ -12,11 +12,11 @@ import androidx.preference.PreferenceManager
import com.deniscerri.ytdl.BuildConfig
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.core.RuntimeManager
import com.deniscerri.ytdl.core.runtimes.Aria2c
import com.deniscerri.ytdl.core.runtimes.BaseRuntime
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.PluginBase
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.database.viewmodel.SettingsViewModel
import com.deniscerri.ytdl.database.viewmodel.YTDLPViewModel
import com.deniscerri.ytdl.ui.more.settings.BaseSettingsFragment
@ -92,6 +92,11 @@ class UpdateSettingsFragment : BaseSettingsFragment() {
false
}
findPreference<Preference>("plugins")?.setOnPreferenceClickListener {
findNavController().navigate(R.id.pluginsFragment)
false
}
version = findPreference("version")
val nativeLibraryDir = context?.applicationInfo?.nativeLibraryDir
@ -116,21 +121,21 @@ class UpdateSettingsFragment : BaseSettingsFragment() {
true
}
//packages
findPreference<Preference>("package_python")?.apply {
//plugins
findPreference<Preference>("plugin_python")?.apply {
val instance = Python.getInstance()
summary = instance.getVersion(requireContext())
}
findPreference<Preference>("package_ffmpeg")?.apply {
findPreference<Preference>("plugin_ffmpeg")?.apply {
val instance = FFmpeg.getInstance()
summary = instance.getVersion(requireContext())
}
findPreference<Preference>("package_aria2c")?.apply {
findPreference<Preference>("plugin_aria2c")?.apply {
val instance = Aria2c.getInstance()
summary = instance.getVersion(requireContext())
}
handlePackage(NodeJS.getInstance(), findPreference<Preference>("package_nodejs"))
handlePlugin(NodeJS.getInstance(), findPreference<Preference>("plugin_nodejs"))
findPreference<Preference>("reset_preferences")?.setOnPreferenceClickListener {
UiUtil.showGenericConfirmDialog(requireContext(), getString(R.string.reset), getString(R.string.reset_preferences_in_screen)) {
@ -206,7 +211,7 @@ class UpdateSettingsFragment : BaseSettingsFragment() {
}
private fun handlePackage(instance: BaseRuntime, preference: Preference?) {
private fun handlePlugin(instance: PluginBase, preference: Preference?) {
//TODO REWRITE THIS LOL
preference?.apply {
summary = instance.getVersion(requireContext())

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.deniscerri.ytdl.ui.more.terminal.TerminalActivity">
<RelativeLayout
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:id="@+id/recycler_view"
android:orientation="vertical"
android:layout_height="wrap_content"
android:scrollbars="none"
android:clipToPadding="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
/>
</RelativeLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView android:id="@+id/log_card_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:backgroundTint="@android:color/transparent"
android:checkable="true"
android:clickable="true"
android:focusable="true"
app:checkedIcon="@null"
app:strokeColor="?attr/colorPrimary"
app:cardPreventCornerOverlap="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar"
app:strokeWidth="0dp"
xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:paddingHorizontal="20dp"
android:paddingVertical="10dp"
android:layout_height="wrap_content">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/plugin_icon"
android:layout_width="30dp"
android:layout_height="30dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
android:src="@drawable/ic_code"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_weight="0.3"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp"
android:clickable="false"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_weight="0.7"
app:layout_constraintStart_toEndOf="@+id/plugin_icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0">
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:clickable="false"
android:ellipsize="end"
android:maxLines="2"
android:paddingHorizontal="5dp"
android:scrollbars="none"
android:textSize="17sp"
android:textStyle="bold"
tools:text="Python"
app:layout_constraintEnd_toStartOf="@+id/download_type"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/version"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:clickable="false"
android:ellipsize="end"
android:maxLines="2"
android:paddingHorizontal="5dp"
android:scrollbars="none"
android:textSize="15sp"
android:textStyle="bold"
android:fontFamily="monospace"
tools:text="v3.12"
app:layout_constraintEnd_toStartOf="@+id/download_type"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/title" />
<com.google.android.material.button.MaterialButton
android:id="@+id/download_type"
style="?attr/materialIconButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:clickable="false"
android:contentDescription="@string/preferred_download_type"
android:minHeight="0dp"
android:padding="0dp"
app:cornerRadius="10dp"
app:icon="@drawable/outline_arrow_forward_24"
app:iconTint="?attr/colorAccent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

@ -0,0 +1,111 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/downloadHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:orientation="horizontal"
android:paddingTop="20dp">
<com.facebook.shimmer.ShimmerFrameLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_marginEnd="20dp"
app:layout_constraintEnd_toStartOf="@+id/bottomsheet_schedule_button"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:id="@+id/shimmer_loading_title"
android:orientation="vertical">
<TextView
android:id="@+id/bottom_sheet_loading_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="2"
android:singleLine="false"
android:text="@string/loading"
android:textSize="25sp" />
</com.facebook.shimmer.ShimmerFrameLayout>
<TextView
android:id="@+id/bottom_sheet_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:maxLines="2"
android:singleLine="false"
android:textSize="20sp"
android:text="@string/plugins"
app:layout_constraintEnd_toStartOf="@+id/bottomsheet_import_zip"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/bottom_sheet_subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/configure_download"
android:textSize="12sp"
android:textStyle="bold"
tools:text="Python"
app:layout_constraintEnd_toStartOf="@+id/bottomsheet_import_zip"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/bottom_sheet_title" />
<Button
android:id="@+id/bottomsheet_import_zip"
style="@style/Widget.Material3.Button.ElevatedButton.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoLink="all"
android:outlineProvider="none"
android:stateListAnimator="@null"
android:text="@string/import_zip"
app:icon="@drawable/baseline_folder_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone"
android:paddingBottom="85dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
<com.google.android.material.progressindicator.CircularProgressIndicator
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:padding="70dp"
android:indeterminate="true" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

@ -510,6 +510,7 @@
<string name="show_quick_download_share_menu">Show option to download immediately in the share menu</string>
<string name="quick_download_title">Don\'t fetch data</string>
<string name="sponsorblock_hook">Hook/Greetings</string>
<string name="packages">Packages</string>
<string name="plugins">Plugins</string>
<string name="not_installed">Not installed</string>
<string name="import_zip">Import ZIP</string>
</resources>

@ -76,28 +76,28 @@
app:title="@string/changelog" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/packages">
<PreferenceCategory android:title="@string/plugins">
<Preference
app:icon="@drawable/outline_data_object_24"
app:key="package_python"
app:key="plugin_python"
app:title="Python"
tools:summary="@string/not_installed" />
<Preference
app:icon="@drawable/outline_data_object_24"
app:key="package_ffmpeg"
app:key="plugin_ffmpeg"
app:title="FFmpeg"
tools:summary="@string/not_installed" />
<Preference
app:icon="@drawable/outline_data_object_24"
app:key="package_aria2c"
app:key="plugin_aria2c"
app:title="Aria2c"
tools:summary="@string/not_installed" />
<Preference
app:icon="@drawable/outline_data_object_24"
app:key="package_nodejs"
app:key="plugin_nodejs"
app:title="NodeJS"
tools:summary="@string/not_installed" />
</PreferenceCategory>

Loading…
Cancel
Save