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