mirror of https://github.com/deniscerri/ytdlnis
wipp
parent
cb19cb4685
commit
56979d78d8
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
Loading…
Reference in New Issue