Compare commits

..

1 commit

Author SHA1 Message Date
firelight
9fa1540e85
fix 2026-10-16 21:59:03 +02:00
44 changed files with 67 additions and 3797 deletions

View file

@ -52,7 +52,6 @@ import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.updateTv import com.lagradost.cloudstream3.ui.settings.Globals.updateTv
import com.lagradost.cloudstream3.ui.settings.extensions.PluginAdapter import com.lagradost.cloudstream3.ui.settings.extensions.PluginAdapter
import com.lagradost.cloudstream3.utils.AppContextUtils.isRtl import com.lagradost.cloudstream3.utils.AppContextUtils.isRtl
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.Event
import com.lagradost.cloudstream3.utils.UIHelper.showInputMethod import com.lagradost.cloudstream3.utils.UIHelper.showInputMethod
@ -267,9 +266,7 @@ object CommonActivity {
} }
// Ask for notification permissions on Android 13 // Ask for notification permissions on Android 13
// Not while biometric locked, the dialog cancels the biometric prompt
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
!isAuthEnabled(componentActivity) &&
ContextCompat.checkSelfPermission( ContextCompat.checkSelfPermission(
componentActivity, componentActivity,
Manifest.permission.POST_NOTIFICATIONS Manifest.permission.POST_NOTIFICATIONS

View file

@ -1379,7 +1379,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
} }
//Automatically download not existing plugins, using mode specified. //Automatically download not existing plugins, using mode specified.
/*val autoDownloadPlugin = AutoDownloadMode.getEnum( val autoDownloadPlugin = AutoDownloadMode.getEnum(
settingsManager.getInt( settingsManager.getInt(
getString(R.string.auto_download_plugins_key), getString(R.string.auto_download_plugins_key),
0 0
@ -1390,7 +1390,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
this@MainActivity, this@MainActivity,
autoDownloadPlugin autoDownloadPlugin
) )
}*/ }
} }
ioSafe { ioSafe {

View file

@ -590,7 +590,7 @@ object PluginManager {
/** /**
* @return True if successful, false if not * @return True if successful, false if not
* */ * */
internal suspend fun loadPlugin(context: Context, file: File, data: PluginData): Boolean { private suspend fun loadPlugin(context: Context, file: File, data: PluginData): Boolean {
val fileName = file.nameWithoutExtension val fileName = file.nameWithoutExtension
val filePath = file.absolutePath val filePath = file.absolutePath
currentlyLoading = fileName currentlyLoading = fileName

View file

@ -161,7 +161,7 @@ object RepositoryManager {
suspend fun parseRepository(url: String): Repository? { suspend fun parseRepository(url: String): Repository? {
return safeAsync { return safeAsync {
// Take manifestVersion and such into account later // Take manifestVersion and such into account later
app.get(convertRawGitUrl(url), cacheTime = 15, cacheUnit = TimeUnit.MINUTES) app.get(convertRawGitUrl(url), cacheTime = 5, cacheUnit = TimeUnit.MINUTES)
.parsedSafe<Repository>() .parsedSafe<Repository>()
} }
} }
@ -169,7 +169,7 @@ object RepositoryManager {
private suspend fun parsePlugins(pluginUrls: String): List<SitePlugin> { private suspend fun parsePlugins(pluginUrls: String): List<SitePlugin> {
// Take manifestVersion and such into account later // Take manifestVersion and such into account later
return try { return try {
app.get(convertRawGitUrl(pluginUrls), cacheTime = 15, cacheUnit = TimeUnit.MINUTES) app.get(convertRawGitUrl(pluginUrls), cacheTime = 5, cacheUnit = TimeUnit.MINUTES)
.parsed<Array<SitePlugin>>().toList() .parsed<Array<SitePlugin>>().toList()
} catch (t: Throwable) { } catch (t: Throwable) {
logError(t) logError(t)
@ -247,16 +247,9 @@ object RepositoryManager {
private val repoLock = Mutex() private val repoLock = Mutex()
suspend fun addRepository(repository: RepositoryData) { suspend fun addRepository(repository: RepositoryData) {
repoLock.withLock { repoLock.withLock {
// No duplicates, but we want the new repository instead of the old one val currentRepos = getRepositories()
var currentRepos = getRepositories() // No duplicates
val index = currentRepos.indexOfFirst { it.url == repository.url } setKey(REPOSITORIES_KEY, (currentRepos + repository).distinctBy { it.url })
if(index == -1) {
currentRepos += repository
} else {
/** We want to keep the ordering here, as that is the addition order */
currentRepos[index] = repository
}
setKey(REPOSITORIES_KEY, currentRepos)
} }
} }

View file

@ -143,7 +143,7 @@ object AccountScreen {
.padding(contentPadding), .padding(contentPadding),
horizontalAlignment = Alignment.End horizontalAlignment = Alignment.End
) { ) {
val (edit, accounts) = remember { FocusRequester.createRefs() } val (edit, accounts) = FocusRequester.createRefs()
IconButton( IconButton(
onClick = { onClick = {
@ -318,7 +318,7 @@ object AccountScreen {
) )
} }
val focusRequester = remember { FocusRequester() } val (focusRequester) = FocusRequester.createRefs()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
focusRequester.requestFocus() focusRequester.requestFocus()
} }
@ -746,7 +746,7 @@ object AccountScreen {
val innerPadding by animateDpAsState( val innerPadding by animateDpAsState(
targetValue = if (hasFocus || isLayout(PHONE or EMULATOR)) 0.dp else MaterialTheme.padding.extraSmall, targetValue = if (hasFocus || isLayout(PHONE or EMULATOR)) 0.dp else MaterialTheme.padding.extraSmall,
) )
val focusRequester = remember { FocusRequester() } val (focusRequester) = FocusRequester.createRefs()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
if (selected) { if (selected) {
focusRequester.requestFocus() focusRequester.requestFocus()

View file

@ -887,6 +887,7 @@ class PlayerGestureHelper(private val playerView: PlayerView) {
val wasShowing = playerView.callbacks?.isUIShowing() ?: false val wasShowing = playerView.callbacks?.isUIShowing() ?: false
// Only expose the parent chain when controls are currently hidden. // Only expose the parent chain when controls are currently hidden.
val prevCenterMenuGone = playerView.playerCenterMenu?.isGone ?: false
val prevVideoHolderVisible = playerView.playerVideoHolder?.isVisible ?: true val prevVideoHolderVisible = playerView.playerVideoHolder?.isVisible ?: true
if (!wasShowing) { if (!wasShowing) {
playerView.playerCenterMenu?.isGone = false playerView.playerCenterMenu?.isGone = false
@ -905,7 +906,7 @@ class PlayerGestureHelper(private val playerView: PlayerView) {
resetRewindText() resetRewindText()
// Restore parent chain only if we changed it and controls are still hidden. // Restore parent chain only if we changed it and controls are still hidden.
if (!wasShowing && !(playerView.callbacks?.isUIShowing() ?: false)) { if (!wasShowing && !(playerView.callbacks?.isUIShowing() ?: false)) {
playerView.playerCenterMenu?.isGone = true playerView.playerCenterMenu?.isGone = prevCenterMenuGone
playerView.playerVideoHolder?.isVisible = prevVideoHolderVisible playerView.playerVideoHolder?.isVisible = prevVideoHolderVisible
rewHolder.alpha = 0f rewHolder.alpha = 0f
} }
@ -926,6 +927,7 @@ class PlayerGestureHelper(private val playerView: PlayerView) {
val ffwdText = playerView.exoFfwdText val ffwdText = playerView.exoFfwdText
val wasShowing = playerView.callbacks?.isUIShowing() ?: false val wasShowing = playerView.callbacks?.isUIShowing() ?: false
val prevCenterMenuGone = playerView.playerCenterMenu?.isGone ?: false
val prevVideoHolderVisible = playerView.playerVideoHolder?.isVisible ?: true val prevVideoHolderVisible = playerView.playerVideoHolder?.isVisible ?: true
if (!wasShowing) { if (!wasShowing) {
playerView.playerCenterMenu?.isGone = false playerView.playerCenterMenu?.isGone = false
@ -943,7 +945,7 @@ class PlayerGestureHelper(private val playerView: PlayerView) {
ffwdText?.post { ffwdText?.post {
resetFastForwardText() resetFastForwardText()
if (!wasShowing && !(playerView.callbacks?.isUIShowing() ?: false)) { if (!wasShowing && !(playerView.callbacks?.isUIShowing() ?: false)) {
playerView.playerCenterMenu?.isGone = true playerView.playerCenterMenu?.isGone = prevCenterMenuGone
playerView.playerVideoHolder?.isVisible = prevVideoHolderVisible playerView.playerVideoHolder?.isVisible = prevVideoHolderVisible
ffwdHolder.alpha = 0f ffwdHolder.alpha = 0f
} }

View file

@ -189,58 +189,6 @@ object ApkUpdater : AppUpdater {
} }
} }
/**
* Returns true if we have a non-null digest that matches a digest on the specified existing file
* */
suspend fun isFileDigest(file: File, digest: DigestPair?): Boolean {
if (digest == null) {
return false
}
try {
val fileDigest = calculateDigest(
digestAlgorithm = digest.algorithm,
readStream = file.inputStream()
)
return fileDigest != null && digest.digest.contentEquals(fileDigest)
} catch (_: Throwable) {
// File not found/security/read issues, not very useful errors
}
return false
}
/**
* Calculates the digest of a stream with the specified algorithm, use transfer instead if
* you already download something.
*
* Will throw read errors
* Will return null if the algorithm is null or does not exist/has some error
* */
@Throws
suspend fun calculateDigest(digestAlgorithm: String?, readStream: InputStream) =
withContext(Dispatchers.IO) {
if (digestAlgorithm == null) return@withContext null
val md =
safe {
MessageDigest.getInstance(digestAlgorithm)
} ?: return@withContext null
val context = currentCoroutineContext()
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var read: Int
while ((readStream.read(buffer, 0, DEFAULT_BUFFER_SIZE)
.also { read = it }) >= 0
) {
try {
md.update(buffer, 0, read)
} catch (_: Throwable) {
return@withContext null
}
context.ensureActive()
}
return@withContext safe { md.digest() }
}
@Throws @Throws
suspend fun legacyDownloader( suspend fun legacyDownloader(
activity: Activity, activity: Activity,

View file

@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
@ -31,6 +30,7 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.rememberSearchBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
@ -47,11 +47,6 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
@ -295,52 +290,23 @@ object SettingsFragmentScreen : Screen {
@Composable @Composable
fun SettingsSearch(textFieldState : TextFieldState) { fun SettingsSearch(textFieldState : TextFieldState) {
var hasFocus by remember { mutableStateOf(false) } var hasFocus by remember { mutableStateOf(false) }
var isWritable by remember { mutableStateOf(false) }
val focusProgress by animateFloatAsState(targetValue = if (hasFocus) 1.0f else 0.0f) val focusProgress by animateFloatAsState(targetValue = if (hasFocus) 1.0f else 0.0f)
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val isTv = isLayout(TV)
TextField( TextField(
state = textFieldState, state = textFieldState,
keyboardOptions = KeyboardOptions.Default.copy( keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Search, imeAction = ImeAction.Search,
/** // This option is bugged on compose, making it unable to open at all
* We do not want to pull up the keyboard when navigating to the item on TV, therefore // showKeyboardOnFocus = false
* we listen to when the TV user presses some key to "extra" focus it
*
* showKeyboardOnFocus is somewhat bugged, so need to change this to true when you
* press it, as otherwise no keyboard will show even with keyboardController
* */
showKeyboardOnFocus = if (isTv) isWritable else true
), ),
lineLimits = TextFieldLineLimits.SingleLine,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 24.dp - 12.dp * focusProgress) .padding(horizontal = 24.dp - 12.dp * focusProgress)
.focusOutline(enabled = isTv, CircleShape) .focusOutline(enabled = isLayout(TV), CircleShape)
.onFocusChanged { newFocus -> .onFocusChanged { newFocus ->
hasFocus = newFocus.hasFocus hasFocus = newFocus.hasFocus
/** Loss of focus -> readonly */ }.focusRequester(focusRequester),
if (!newFocus.hasFocus) {
isWritable = false
}
}
.focusRequester(focusRequester)
.onPreviewKeyEvent { keyEvent ->
if (isWritable || !isTv || !hasFocus || keyEvent.type != KeyEventType.KeyUp) {
return@onPreviewKeyEvent false
}
/** Focus + press once -> writable */
return@onPreviewKeyEvent when (keyEvent.key) {
Key.Enter, Key.DirectionCenter, Key.NumPadEnter, Key.Spacebar -> {
isWritable = true
true
}
else -> false
}
},
placeholder = { placeholder = {
Text(text = stringResource(R.string.search_hint)) Text(text = stringResource(R.string.search_hint))
}, },

View file

@ -13,15 +13,17 @@ import androidx.compose.ui.res.integerArrayResource
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringArrayResource import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import com.lagradost.cloudstream3.AutoDownloadMode
import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.BuildConfig
import com.lagradost.cloudstream3.CloudStreamApp import com.lagradost.cloudstream3.CloudStreamApp
import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.CommonActivity.activity
import com.lagradost.cloudstream3.MainActivityScreen import com.lagradost.cloudstream3.MainActivityScreen
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.services.BackupWorkManager import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.utils.BackupUtils import com.lagradost.cloudstream3.utils.BackupUtils
import com.lagradost.cloudstream3.utils.BackupUtils.restorePrompt import com.lagradost.cloudstream3.utils.BackupUtils.restorePrompt
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.InAppUpdater.installPreReleaseIfNeeded import com.lagradost.cloudstream3.utils.InAppUpdater.installPreReleaseIfNeeded
import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream4.AppSettings import com.lagradost.cloudstream4.AppSettings
@ -145,14 +147,7 @@ object SettingsUpdatesScreen : SearchableSettings {
entries = integerArrayResource(R.array.periodic_work_values).zip( entries = integerArrayResource(R.array.periodic_work_values).zip(
stringArrayResource(R.array.periodic_work_names) stringArrayResource(R.array.periodic_work_names)
).toMap(), ).toMap(),
preference = settings.backup.frequency, preference = settings.backup.frequency
onValueChanged = { value ->
BackupWorkManager.enqueuePeriodicWork(
CloudStreamApp.context,
value.toLong()
)
return@ListPreference true
}
), ),
Preference.PreferenceItem.TextPreference( Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.backup_path_title), title = stringResource(R.string.backup_path_title),
@ -183,7 +178,7 @@ object SettingsUpdatesScreen : SearchableSettings {
preference = settings.plugins.autoUpdate, preference = settings.plugins.autoUpdate,
), ),
/*Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
title = stringResource(R.string.automatic_plugin_download), title = stringResource(R.string.automatic_plugin_download),
subtitle = "%s\n" + stringResource(R.string.automatic_plugin_download_summary), subtitle = "%s\n" + stringResource(R.string.automatic_plugin_download_summary),
icon = painterResource(R.drawable.extention_renew2), icon = painterResource(R.drawable.extention_renew2),
@ -202,7 +197,7 @@ object SettingsUpdatesScreen : SearchableSettings {
) )
} }
} }
),*/ ),
Preference.PreferenceItem.TextPreference( Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.test_extensions), title = stringResource(R.string.test_extensions),
subtitle = stringResource(R.string.test_extensions_summary), subtitle = stringResource(R.string.test_extensions_summary),

View file

@ -1,282 +0,0 @@
package com.lagradost.cloudstream3.ui.settings.extensions
import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey
import com.lagradost.cloudstream3.CommonActivity
import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.InternalAPI
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.cmap
import com.lagradost.cloudstream3.plugins.PLUGIN_VERSION_NOT_SET
import com.lagradost.cloudstream3.plugins.PluginData
import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.plugins.PluginManager.deletePlugin
import com.lagradost.cloudstream3.plugins.PluginManager.getPluginPath
import com.lagradost.cloudstream3.plugins.PluginManager.unloadPlugin
import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES
import com.lagradost.cloudstream3.plugins.RepositoryManager.addRepository
import com.lagradost.cloudstream3.plugins.RepositoryManager.convertRawGitUrl
import com.lagradost.cloudstream3.plugins.RepositoryManager.parseRepoUrl
import com.lagradost.cloudstream3.plugins.RepositoryManager.removeRepository
import com.lagradost.cloudstream3.plugins.SitePlugin
import com.lagradost.cloudstream3.ui.settings.ApkUpdater
import com.lagradost.cloudstream3.ui.settings.ApkUpdater.isFileDigest
import com.lagradost.cloudstream4.viewmodels.DigestPair
import com.lagradost.cloudstream4.viewmodels.Plugin
import com.lagradost.cloudstream4.viewmodels.PluginDownloadSource
import com.lagradost.cloudstream4.viewmodels.PluginInfo
import com.lagradost.cloudstream4.viewmodels.PluginStatus
import com.lagradost.cloudstream4.viewmodels.RepositoryInfo
import com.lagradost.cloudstream4.viewmodels.RepositoryManager
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.nio.file.AtomicMoveNotSupportedException
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.util.concurrent.TimeUnit
import kotlin.uuid.Uuid
@InternalAPI
object AndroidRepositoryManager : RepositoryManager {
fun RepositoryInfo.toData() =
RepositoryData(
name = name,
url = url,
iconUrl = iconUrl,
description = description
)
fun RepositoryData.toInfo() =
RepositoryInfo(
name = name,
url = url,
iconUrl = iconUrl,
description = description
)
override fun getRepositories(): List<RepositoryInfo> {
return ((getKey<Array<RepositoryData>>(REPOSITORIES_KEY)
?: emptyArray()) + PREBUILT_REPOSITORIES).map { info ->
info.toInfo()
}
}
override suspend fun getPlugins(
repository: RepositoryInfo,
repositoryUuid: Uuid
): Pair<RepositoryInfo, List<Plugin>> =
withContext(Dispatchers.IO) {
val activity =
CommonActivity.activity ?: throw ErrorLoadingException("No activity found")
val url = parseRepoUrl(repository.url)
if (url.isNullOrBlank()) {
throw ErrorLoadingException(activity.getString(R.string.error_invalid_url))
}
val parsedRepository = app.get(
url = convertRawGitUrl(url),
cacheTime = 15,
cacheUnit = TimeUnit.MINUTES
).parsed<com.lagradost.cloudstream3.plugins.Repository>()
/**
* No sane repository should be able to DDoS the app when navigating to settings, so
* we limit this to a maximum of 10
*
* Note that this list is on all verified repos = 1, as pluginLists is a list of lists
* */
require(parsedRepository.pluginLists.size <= 10)
/** Add this as a new repository -> replaces the old data with new icon,name and description */
addRepository(
RepositoryData(
iconUrl = parsedRepository.iconUrl,
url = url,
name = parsedRepository.name,
description = parsedRepository.description
)
)
val plugins = parsedRepository.pluginLists.cmap { url ->
/** This should really be a list of size ~ 1 */
app.get(
url = convertRawGitUrl(url),
cacheTime = 15,
cacheUnit = TimeUnit.MINUTES
).parsed<Array<SitePlugin>>()
}.flatMap { plugins ->
plugins.map { plugin ->
val file = getPluginPath(
context = activity,
internalName = plugin.internalName,
repositoryUrl = url
)
val language = plugin.language?.trim()
Plugin(
info = PluginInfo(
name = plugin.name,
status = plugin.status,
internalName = plugin.internalName,
version = plugin.version,
tvTypes = plugin.tvTypes?.mapNotNull { runCatching { TvType.valueOf(it) }.getOrNull() }
?.toPersistentList()
?: persistentListOf(),
authors = plugin.authors.toPersistentList(),
iconUrl = plugin.iconUrl,
language = if (language.isNullOrBlank()) {
null
} else {
language
},
description = plugin.description,
repositoryUrl = plugin.repositoryUrl
),
source = PluginDownloadSource(
size = plugin.fileSize,
repositoryUrl = url,
pluginUrl = plugin.url,
internalName = plugin.internalName,
digest = DigestPair.parse(plugin.fileHash)
),
status = PluginStatus.Idle,
downloadLocation = if (file.exists()) {
file.absolutePath
} else {
null
},
repository = repositoryUuid,
)
}
}
val info = RepositoryInfo(
iconUrl = parsedRepository.iconUrl,
url = url,
name = parsedRepository.name,
description = parsedRepository.description,
)
info to plugins
}
override suspend fun downloadPlugin(
source: PluginDownloadSource,
downloadProgress: (Long, Long?) -> Unit
): String = withContext(Dispatchers.IO) {
val context = CommonActivity.activity ?: throw ErrorLoadingException("No activity found")
val file = getPluginPath(
context = context,
internalName = source.internalName,
repositoryUrl = source.repositoryUrl
)
/**
* Before we do anything that needs a network connection,
* we actually check if we need to update it
*
* Because if the file digest is correct, then why do we need to do a network connection?
* */
if (isFileDigest(file, source.digest)) {
return@withContext file.absolutePath
}
val tempFile = File.createTempFile(file.name, ".tmp", context.cacheDir)
tempFile.outputStream().use { writeStream ->
app.get(convertRawGitUrl(source.pluginUrl)).body.use { body ->
val readStream = body.byteStream()
ApkUpdater.transfer(
writeStream = writeStream,
readStream = readStream,
length = source.size,
downloadProgress = downloadProgress,
digest = source.digest
)
}
}
// Always load the plugin
unloadPlugin(file.absolutePath)
file.delete() // delete if we somehow already have this downloaded?
file.parentFile?.mkdirs() // make the directories as Files.move needs this to work
try {
Files.move(
tempFile.toPath(),
file.toPath(),
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.ATOMIC_MOVE
)
} catch (_: AtomicMoveNotSupportedException) {
Files.move(
tempFile.toPath(),
file.toPath(),
StandardCopyOption.REPLACE_EXISTING
)
}
val result = PluginManager.loadPlugin(
context = context,
file = file,
data = PluginData(
internalName = source.internalName,
url = source.pluginUrl,
isOnline = true,
filePath = file.absolutePath,
version = PLUGIN_VERSION_NOT_SET
)
)
if (!result) {
throw ErrorLoadingException("Unable to load plugin ${source.internalName}")
}
return@withContext file.absolutePath
}
override fun getLocalPlugins(localUuid: Uuid): List<Plugin> {
val plugins = PluginManager.getPluginsLocal()
return plugins.map { plugin ->
Plugin(
info = PluginInfo(
name = plugin.internalName,
internalName = plugin.internalName,
version = plugin.version,
tvTypes = persistentListOf(),
authors = persistentListOf(),
status = 1,
iconUrl = null,
language = null,
description = null,
repositoryUrl = null
),
source = null,
status = PluginStatus.Idle,
downloadLocation = plugin.filePath,
repository = localUuid,
)
}
}
override suspend fun deletePlugin(location: String) {
deletePlugin(File(location))
}
override suspend fun deleteRepository(repository: RepositoryInfo) {
val context = CommonActivity.activity ?: throw ErrorLoadingException("No activity found")
removeRepository(
context,
repository.toData()
)
}
override suspend fun updateRepositoryInfo(repository: RepositoryInfo) {
addRepository(
repository.toData()
)
}
}

View file

@ -302,7 +302,7 @@ class ExtensionsFragment : BaseFragment<FragmentExtensionsBinding>(
val fixedName = if (!name.isNullOrBlank()) name val fixedName = if (!name.isNullOrBlank()) name
else repository.name else repository.name
val newRepo = RepositoryData(repository.iconUrl, fixedName, url, repository.description) val newRepo = RepositoryData(repository.iconUrl, fixedName, url)
RepositoryManager.addRepository(newRepo) RepositoryManager.addRepository(newRepo)
extensionViewModel.loadStats() extensionViewModel.loadStats()
extensionViewModel.loadRepositories() extensionViewModel.loadRepositories()

View file

@ -1,6 +0,0 @@
package com.lagradost.cloudstream3.ui.settings.extensions
import com.lagradost.cloudstream3.utils.BaseComposeFragment
import com.lagradost.cloudstream4.compose.Screen
class ExtensionsFragment2 : BaseComposeFragment(), Screen by ExtensionsScreen

View file

@ -23,21 +23,8 @@ data class RepositoryData(
@JsonProperty("iconUrl") @SerialName("iconUrl") val iconUrl: String?, @JsonProperty("iconUrl") @SerialName("iconUrl") val iconUrl: String?,
@JsonProperty("name") @SerialName("name") val name: String, @JsonProperty("name") @SerialName("name") val name: String,
@JsonProperty("url") @SerialName("url") val url: String, @JsonProperty("url") @SerialName("url") val url: String,
@JsonProperty("description") @SerialName("description") val description: String? = null,
) { ) {
constructor(name: String, url: String) : this( constructor(name: String, url: String): this(null, name, url)
iconUrl = null,
name = name,
url = url,
description = null
)
constructor(iconUrl: String?, name: String, url: String) : this(
iconUrl = iconUrl,
name = name,
url = url,
description = null
)
} }
const val REPOSITORIES_KEY = "REPOSITORIES_KEY" const val REPOSITORIES_KEY = "REPOSITORIES_KEY"

View file

@ -527,8 +527,7 @@ object AppContextUtils {
val data = RepositoryData( val data = RepositoryData(
repo.iconUrl ?: "", repo.iconUrl ?: "",
repo.name, repo.name,
url, url
repo.description
) )
RepositoryManager.addRepository(data) RepositoryManager.addRepository(data)
main { main {

View file

@ -1045,7 +1045,7 @@ object VideoDownloadManager {
) )
) )
// too short file, treat it as a invalid link // too short file, treat it as an invalid link
if (items.totalLength != null && items.totalLength < minimumSize) { if (items.totalLength != null && items.totalLength < minimumSize) {
fileStream.closeQuietly() fileStream.closeQuietly()
metadata.onDelete() metadata.onDelete()
@ -1241,13 +1241,15 @@ object VideoDownloadManager {
metadata.type = DownloadType.IsDone metadata.type = DownloadType.IsDone
return@withContext DOWNLOAD_SUCCESS return@withContext DOWNLOAD_SUCCESS
} catch (e: IOException) { } catch (e: IOException) {
// some sort of IO error, this should not happened // some sort of IO error, this usually happens when storage access is revoked
// we just rethrow it // we just rethrow it
showToast(R.string.error_file_io_toast)
logError(e) logError(e)
throw e throw e
} catch (t: Throwable) { } catch (t: Throwable) {
// some sort of network error, will error // some sort of network error, will error
logError(t) logError(t)
showToast(R.string.error_file_unknown_toast)
// note that when failing we don't want to delete the file, // note that when failing we don't want to delete the file,
// only user interaction has that power // only user interaction has that power
metadata.type = DownloadType.IsFailed metadata.type = DownloadType.IsFailed
@ -1818,6 +1820,9 @@ object VideoDownloadManager {
isCompleted = true isCompleted = true
break break
} else if (!connectionResult.tryNext || index >= item.links.lastIndex) { } else if (!connectionResult.tryNext || index >= item.links.lastIndex) {
if (connectionResult == DOWNLOAD_INVALID_INPUT) {
showToast(R.string.error_file_invalid_toast)
}
isFailed = true isFailed = true
break break
} }

View file

@ -215,7 +215,9 @@ object DownloadObjects {
} }
fun delete(): Boolean { fun delete(): Boolean {
return file.delete() == true return runCatching {
file.delete()
}.getOrNull() == true
} }
val resume: Boolean get() = fileLength > 0L val resume: Boolean get() = fileLength > 0L

View file

@ -32,10 +32,10 @@ class TheIntroDBSkip : SkipAPI() {
?: return null ?: return null
val url = if (data.isMovie()) { val url = if (data.isMovie()) {
"$mainUrl/v4/media?$idSuffix" "$mainUrl/v2/media?$idSuffix"
} else { } else {
val season = episode.season ?: return null val season = episode.season ?: return null
"$mainUrl/v4/media?$idSuffix&season=$season&episode=${episode.episode}" "$mainUrl/v2/media?$idSuffix&season=$season&episode=${episode.episode}"
} }
val root = app.get(url).parsed<Root>() val root = app.get(url).parsed<Root>()
return arrayOf( return arrayOf(
@ -43,7 +43,7 @@ class TheIntroDBSkip : SkipAPI() {
root.credits to SkipType.Credits, root.credits to SkipType.Credits,
root.recap to SkipType.Recap, root.recap to SkipType.Recap,
root.preview to SkipType.Preview root.preview to SkipType.Preview
).flatMap { (list, type) -> ).map { (list, type) ->
list.map { stamp -> list.map { stamp ->
SkipStamp( SkipStamp(
type, type,
@ -51,11 +51,12 @@ class TheIntroDBSkip : SkipAPI() {
stamp.endMs ?: episodeDurationMs stamp.endMs ?: episodeDurationMs
) )
} }
} }.flatten()
} }
@Serializable @Serializable
data class Root( data class Root(
@JsonProperty("tmdb_id") @SerialName("tmdb_id") val tmdbId: Long,
@JsonProperty("type") @SerialName("type") val type: String, @JsonProperty("type") @SerialName("type") val type: String,
@JsonProperty("intro") @SerialName("intro") val intro: List<Stamp> = emptyList(), @JsonProperty("intro") @SerialName("intro") val intro: List<Stamp> = emptyList(),
@JsonProperty("recap") @SerialName("recap") val recap: List<Stamp> = emptyList(), @JsonProperty("recap") @SerialName("recap") val recap: List<Stamp> = emptyList(),

View file

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M440,488L404,452Q393,441 376,441Q359,441 348,452Q337,463 337,480Q337,497 348,508L452,612Q464,624 480,624Q496,624 508,612L612,508Q623,497 623,480Q623,463 612,452Q601,441 584,441Q567,441 556,452L520,488L520,360Q520,343 508.5,331.5Q497,320 480,320Q463,320 451.5,331.5Q440,343 440,360L440,488ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
</vector>

View file

@ -1,51 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="24dp"
android:height="24dp"
android:viewportWidth="512"
android:viewportHeight="512"
android:tint="#fff">
<path
android:fillAlpha="0.5"
android:pathData="m261.69,163.87a1.2,1.61 90,0 0,-1.56 1.43l71.1,266.49a184.55,184.55 0,0 0,83.36 -71.47l-151.49,-195.85a1.2,1.61 90,0 0,-1.41 -0.59z"
android:strokeWidth="7.79"
android:strokeAlpha="0.5"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter">
<aapt:attr name="android:fillColor">
<gradient
android:endX="380.34"
android:endY="400.66"
android:startX="261.69"
android:startY="163.87"
android:type="linear">
<item
android:color="#00FFFFFF"
android:offset="0" />
<item
android:color="#FF71C2FF"
android:offset="0.5" />
<item
android:color="#FF39AAFF"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#2185d0"
android:pathData="m257.53,77.74a184.55,184.55 0,0 0,-183.86 184.55,184.55 184.55,0 0,0 28.18,98.02l153.87,-198.93a2.15,2.88 90,0 1,4.99 0l153.88,198.94a184.55,184.55 0,0 0,28.18 -98.03,184.55 184.55,45 0,0 -184.55,-184.55 184.55,184.55 0,0 0,-0.69 0z"
android:strokeWidth="2.01453"
android:strokeColor="#00000000" />
</vector>

View file

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M200,760Q167,760 143.5,736.5Q120,713 120,680L120,320L120,320Q103,320 91.5,308.5Q80,297 80,280Q80,263 91.5,251.5Q103,240 120,240L240,240L240,220Q240,203 251.5,191.5Q263,180 280,180L360,180Q377,180 388.5,191.5Q400,203 400,220L400,240L520,240Q537,240 548.5,251.5Q560,263 560,280Q560,297 548.5,308.5Q537,320 520,320L520,320L520,680Q520,713 496.5,736.5Q473,760 440,760L200,760ZM640,720Q623,720 611.5,708.5Q600,697 600,680Q600,663 611.5,651.5Q623,640 640,640L720,640Q737,640 748.5,651.5Q760,663 760,680Q760,697 748.5,708.5Q737,720 720,720L640,720ZM640,560Q623,560 611.5,548.5Q600,537 600,520Q600,503 611.5,491.5Q623,480 640,480L800,480Q817,480 828.5,491.5Q840,503 840,520Q840,537 828.5,548.5Q817,560 800,560L640,560ZM640,400Q623,400 611.5,388.5Q600,377 600,360Q600,343 611.5,331.5Q623,320 640,320L840,320Q857,320 868.5,331.5Q880,343 880,360Q880,377 868.5,388.5Q857,400 840,400L640,400ZM200,320L200,680Q200,680 200,680Q200,680 200,680L440,680Q440,680 440,680Q440,680 440,680L440,320L200,320Z"/>
</vector>

View file

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M480,536L596,652Q607,663 624,663Q641,663 652,652Q663,641 663,624Q663,607 652,596L536,480L652,364Q663,353 663,336Q663,319 652,308Q641,297 624,297Q607,297 596,308L480,424L364,308Q353,297 336,297Q319,297 308,308Q297,319 297,336Q297,353 308,364L424,480L308,596Q297,607 297,624Q297,641 308,652Q319,663 336,663Q353,663 364,652L480,536ZM200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM200,760L760,760Q760,760 760,760Q760,760 760,760L760,200Q760,200 760,200Q760,200 760,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760ZM200,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760L200,760Q200,760 200,760Q200,760 200,760L200,200Q200,200 200,200Q200,200 200,200Z"/>
</vector>

View file

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M508.5,668.5Q520,657 520,640Q520,623 508.5,611.5Q497,600 480,600Q463,600 451.5,611.5Q440,623 440,640Q440,657 451.5,668.5Q463,680 480,680Q497,680 508.5,668.5ZM508.5,508.5Q520,497 520,480L520,320Q520,303 508.5,291.5Q497,280 480,280Q463,280 451.5,291.5Q440,303 440,320L440,480Q440,497 451.5,508.5Q463,520 480,520Q497,520 508.5,508.5ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
</vector>

View file

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M800,686L720,606L720,576L765,554Q781,546 790.5,532Q800,518 800,500Q800,483 790.5,468.5Q781,454 765,446L720,425L720,240Q720,240 720,240Q720,240 720,240L528,240L518,172Q515,150 498.5,135Q482,120 460,120Q437,120 420.5,135Q404,150 401,172L391,240L354,240L274,160L322,160Q329,109 368,74.5Q407,40 460,40Q512,40 551,74.5Q590,109 598,160L720,160Q753,160 776.5,183.5Q800,207 800,240L800,374Q836,392 858,426Q880,460 880,500Q880,541 858,575Q836,609 800,626L800,686ZM720,760L800,840L200,840Q166,840 143,817Q120,794 120,760L120,635Q120,624 128,615.5Q136,607 147,605Q186,597 213,568.5Q240,540 240,500Q240,461 213,433Q186,405 147,396Q136,393 128,384.5Q120,376 120,365L120,160L200,240L200,326Q256,347 288,394Q320,441 320,500Q320,560 288,607Q256,654 200,675L200,760Q200,760 200,760Q200,760 200,760L720,760ZM577,463L577,463L577,463Q577,463 577,463Q577,463 577,463Q577,463 577,463Q577,463 577,463L577,463L577,463Q577,463 577,463Q577,463 577,463L577,463L577,463Q577,463 577,463Q577,463 577,463Q577,463 577,463Q577,463 577,463L577,463L577,463ZM386,500L386,500Q386,500 386,500Q386,500 386,500L386,500Q386,500 386,500Q386,500 386,500Q386,500 386,500Q386,500 386,500L386,500ZM848,904Q836,916 819.5,916Q803,916 791,904L55,169Q43,157 43.5,140.5Q44,124 56,112Q68,100 84.5,100Q101,100 113,112L848,848Q860,860 860,876Q860,892 848,904Z"/>
</vector>

File diff suppressed because one or more lines are too long

View file

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M558,816Q558,816 558,816Q558,816 558,816L796,742Q791,733 781.5,726.5Q772,720 760,720L558,720Q531,720 515,718Q499,716 482,710L425,691Q409,686 402,671Q395,656 400,640Q405,624 419.5,616.5Q434,609 450,614L492,628Q509,633 530.5,636Q552,639 589,640L593,640Q595,640 596.5,640Q598,640 600,640Q600,629 593.5,619Q587,609 578,606L344,520Q344,520 344,520Q344,520 344,520L280,520L280,740L558,816ZM537,894L280,822L280,822Q272,848 248.5,864Q225,880 200,880L120,880Q87,880 63.5,856.5Q40,833 40,800L40,520Q40,487 63.5,463.5Q87,440 120,440L344,440Q351,440 358,441.5Q365,443 371,445L606,532Q639,544 659.5,574Q680,604 680,640L760,640Q810,640 845,673Q880,706 880,760Q880,782 868.5,794.5Q857,807 833,815L583,893Q572,897 560,897Q548,897 537,894ZM120,800Q120,800 120,800Q120,800 120,800L200,800Q200,800 200,800Q200,800 200,800L200,520Q200,520 200,520Q200,520 200,520L120,520Q120,520 120,520Q120,520 120,520L120,800ZM610.5,482.5Q596,477 584,466L474,358Q443,328 421.5,291.5Q400,255 400,212Q400,157 438.5,118.5Q477,80 532,80Q564,80 592,93.5Q620,107 640,130Q660,107 688,93.5Q716,80 748,80Q803,80 841.5,118.5Q880,157 880,212Q880,255 859,291.5Q838,328 807,358L696,466Q684,477 669.5,482.5Q655,488 640,488Q625,488 610.5,482.5ZM640,408L749,301Q768,282 784,260.5Q800,239 800,212Q800,190 785,175Q770,160 748,160Q734,160 721.5,165.5Q709,171 700,182L671,217Q659,231 640,231Q621,231 609,217L580,182Q571,171 558.5,165.5Q546,160 532,160Q510,160 495,175Q480,190 480,212Q480,239 496,260.5Q512,282 531,301L640,408Q640,408 640,408Q640,408 640,408ZM640,254Q640,254 640,254Q640,254 640,254L640,254Q640,254 640,254Q640,254 640,254Q640,254 640,254Q640,254 640,254Q640,254 640,254Q640,254 640,254L640,254Q640,254 640,254Q640,254 640,254L640,254Q640,254 640,254Q640,254 640,254Q640,254 640,254Q640,254 640,254Q640,254 640,254Q640,254 640,254Z"/>
</vector>

View file

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M109,840Q98,840 89,834.5Q80,829 75,820Q70,811 69.5,800.5Q69,790 75,780L445,140Q451,130 460.5,125Q470,120 480,120Q490,120 499.5,125Q509,130 515,140L885,780Q891,790 890.5,800.5Q890,811 885,820Q880,829 871,834.5Q862,840 851,840L109,840ZM178,760L782,760L480,240L178,760ZM508.5,708.5Q520,697 520,680Q520,663 508.5,651.5Q497,640 480,640Q463,640 451.5,651.5Q440,663 440,680Q440,697 451.5,708.5Q463,720 480,720Q497,720 508.5,708.5ZM508.5,588.5Q520,577 520,560L520,440Q520,423 508.5,411.5Q497,400 480,400Q463,400 451.5,411.5Q440,423 440,440L440,560Q440,577 451.5,588.5Q463,600 480,600Q497,600 508.5,588.5ZM480,500L480,500L480,500Z"/>
</vector>

View file

@ -196,7 +196,7 @@
<fragment <fragment
android:id="@+id/navigation_settings_extensions" android:id="@+id/navigation_settings_extensions"
android:name="com.lagradost.cloudstream3.ui.settings.extensions.ExtensionsFragment2" android:name="com.lagradost.cloudstream3.ui.settings.extensions.ExtensionsFragment"
android:label="@string/title_settings" android:label="@string/title_settings"
app:enterAnim="@anim/enter_anim" app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim" app:exitAnim="@anim/exit_anim"

View file

@ -83,6 +83,9 @@
<string name="stream">Network stream</string> <string name="stream">Network stream</string>
<string name="open_local_video">Open local video</string> <string name="open_local_video">Open local video</string>
<string name="error_loading_links_toast">Error Loading Links</string> <string name="error_loading_links_toast">Error Loading Links</string>
<string name="error_file_io_toast">Error occurred when writing to the file. Try setting a new download directory.</string>
<string name="error_file_unknown_toast">Error occurred when downloading the file. Try again.</string>
<string name="error_file_invalid_toast">Download source is invalid. Try another source.</string>
<string name="links_reloaded_toast">Links Reloaded</string> <string name="links_reloaded_toast">Links Reloaded</string>
<string name="download_storage_text">Internal Storage</string> <string name="download_storage_text">Internal Storage</string>
<string name="app_dubbed_text">Dub</string> <string name="app_dubbed_text">Dub</string>
@ -813,5 +816,4 @@
<item quantity="other">%d downloads queued</item> <item quantity="other">%d downloads queued</item>
</plurals> </plurals>
<string name="player_is_live">Live</string> <string name="player_is_live">Live</string>
<string name="installed_plugins">Installed plugins</string>
</resources> </resources>

View file

@ -128,7 +128,7 @@ fun Throwable.getAllMessages(): String {
} }
fun Throwable.getStackTracePretty(showMessage: Boolean = true): String { fun Throwable.getStackTracePretty(showMessage: Boolean = true): String {
val prefix = if (showMessage) this.message?.let { "$it\n" } ?: "" else "" val prefix = if (showMessage) this.message?.let { "\n$it" } ?: "" else ""
return prefix + this.stackTraceToString() return prefix + this.stackTraceToString()
.lines() .lines()
.mapNotNull { line -> .mapNotNull { line ->

View file

@ -1,23 +0,0 @@
package com.lagradost.cloudstream4
import android.content.ClipData
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.Clipboard
actual suspend fun Clipboard.getPlainText(): String? =
getClipEntry()?.clipData?.getItemAt(0)?.text?.toString()
actual suspend fun Clipboard.setPlainText(
label: String,
text: String
) {
setClipEntry(
ClipEntry(
ClipData.newPlainText(
label,
text
)
)
)
}

View file

@ -1,6 +0,0 @@
package com.lagradost.cloudstream4
import androidx.compose.ui.platform.Clipboard
expect suspend fun Clipboard.setPlainText(label: String, text: String)
expect suspend fun Clipboard.getPlainText(): String?

View file

@ -34,7 +34,6 @@ class AppSettings internal constructor(
class PluginPreferences(preferences: PreferenceStore) { class PluginPreferences(preferences: PreferenceStore) {
val autoUpdate = preferences.getBoolean("auto_update_plugins", true) val autoUpdate = preferences.getBoolean("auto_update_plugins", true)
/** Not used anymore, as a user should never auto install all plugins in a repository */
val autoDownload = preferences.getInt("auto_download_plugins_key2", 0) val autoDownload = preferences.getInt("auto_download_plugins_key2", 0)
} }

View file

@ -2,15 +2,10 @@ package com.lagradost.cloudstream4.compose
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonColors
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonColors
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -20,7 +15,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.lagradost.cloudstream4.generated.resources.Res import com.lagradost.cloudstream4.generated.resources.Res
import com.lagradost.cloudstream4.generated.resources.check import com.lagradost.cloudstream4.generated.resources.check
@ -55,26 +49,6 @@ fun BlackButton(
) )
} }
@Composable
fun WhiteIconButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
colors: IconButtonColors = IconButtonDefaults.iconButtonColors(),
interactionSource: MutableInteractionSource? = null,
shape: Shape = IconButtonDefaults.standardShape,
content: @Composable () -> Unit,
) =
IconButton(
onClick = onClick,
modifier = modifier.focusOutline(shape = CircleShape),
enabled = enabled,
colors = colors,
interactionSource = interactionSource,
shape = shape,
content = content
)
@Composable @Composable
fun BaseButton( fun BaseButton(
text: String, onClick: () -> Unit, buttonColors: ButtonColors, text: String, onClick: () -> Unit, buttonColors: ButtonColors,

View file

@ -22,7 +22,6 @@ import org.jetbrains.compose.resources.painterResource
fun WhiteFilterChip( fun WhiteFilterChip(
selected: Boolean, selected: Boolean,
label: String, label: String,
modifier: Modifier = Modifier,
enabled: Boolean = true, enabled: Boolean = true,
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
@ -32,24 +31,16 @@ fun WhiteFilterChip(
border = FilterChipDefaults.filterChipBorder( border = FilterChipDefaults.filterChipBorder(
enabled = enabled, enabled = enabled,
selected = selected, selected = selected,
borderColor = if (hasFocus) { borderColor = MaterialTheme.colorScheme.surfaceVariant,
MaterialTheme.colorScheme.onBackground
} else {
MaterialTheme.colorScheme.surfaceVariant
},
disabledBorderColor = MaterialTheme.colorScheme.surfaceVariant, disabledBorderColor = MaterialTheme.colorScheme.surfaceVariant,
selectedBorderColor = Color.Transparent, selectedBorderColor = Color.Transparent,
disabledSelectedBorderColor = MaterialTheme.colorScheme.surfaceVariant, disabledSelectedBorderColor = MaterialTheme.colorScheme.surfaceVariant,
), ),
modifier = modifier.onFocusChanged { newFocus -> modifier = Modifier.onFocusChanged { newFocus ->
hasFocus = canHaveFocus && newFocus.hasFocus hasFocus = canHaveFocus && newFocus.hasFocus
}, },
selected = selected, selected = selected,
colors = if (hasFocus) { colors = Colors.whiteChip,
Colors.whiteChipFocused
} else {
Colors.whiteChip
},
label = { label = {
Text(label) Text(label)
}, },

View file

@ -38,24 +38,6 @@ object Colors {
disabledLeadingIconColor = MaterialTheme.colorScheme.onSurfaceVariant, disabledLeadingIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
disabledTrailingIconColor = MaterialTheme.colorScheme.onSurfaceVariant, disabledTrailingIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
selectedContainerColor = MaterialTheme.colorScheme.onBackground,
selectedLabelColor = MaterialTheme.colorScheme.background,
selectedLeadingIconColor = MaterialTheme.colorScheme.background,
selectedTrailingIconColor = MaterialTheme.colorScheme.background,
)
internal val whiteChipFocused
@Composable @ReadOnlyComposable get() = SelectableChipColors(
containerColor = Color.Transparent,
labelColor = MaterialTheme.colorScheme.onBackground,
leadingIconColor = MaterialTheme.colorScheme.onBackground,
trailingIconColor = MaterialTheme.colorScheme.onBackground,
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant,
disabledSelectedContainerColor = MaterialTheme.colorScheme.surfaceVariant,
disabledLabelColor = MaterialTheme.colorScheme.onSurfaceVariant,
disabledLeadingIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
disabledTrailingIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
selectedContainerColor = MaterialTheme.colorScheme.onBackground, selectedContainerColor = MaterialTheme.colorScheme.onBackground,
selectedLabelColor = MaterialTheme.colorScheme.background, selectedLabelColor = MaterialTheme.colorScheme.background,
selectedLeadingIconColor = MaterialTheme.colorScheme.background, selectedLeadingIconColor = MaterialTheme.colorScheme.background,

View file

@ -1,16 +1,12 @@
package com.lagradost.cloudstream4.compose package com.lagradost.cloudstream4.compose
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.AlertDialogDefaults import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@ -67,9 +63,7 @@ fun ActionDialog(
} }
} }
}, },
text = { text = { Text(text = text) },
Text(text = text, modifier = Modifier.verticalScroll(rememberScrollState()))
},
confirmButton = { confirmButton = {
WhiteButton(text = confirmText, onClick = confirm) WhiteButton(text = confirmText, onClick = confirm)
}, },

View file

@ -1,26 +0,0 @@
package com.lagradost.cloudstream4.compose
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@Composable
fun LazyGridState.IsScrolling(up : Runnable, down : Runnable) {
LaunchedEffect(lastScrolledBackward) {
if(lastScrolledBackward) up.run()
}
LaunchedEffect(lastScrolledForward) {
if(lastScrolledForward) down.run()
}
}
@Suppress("FrequentlyChangingValue")
@Composable
fun LazyListState.IsScrolling(up : Runnable, down : Runnable) {
LaunchedEffect(firstVisibleItemIndex) {
if(lastScrolledBackward) up.run()
}
LaunchedEffect(firstVisibleItemIndex) {
if(lastScrolledForward) down.run()
}
}

View file

@ -1,6 +1,5 @@
package com.lagradost.cloudstream4.compose package com.lagradost.cloudstream4.compose
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField import androidx.compose.material3.TextField
@ -13,8 +12,6 @@ fun BlackTextField(
placeHolder : String, placeHolder : String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
singleLine: Boolean = false,
onValueChange: (String) -> Unit, onValueChange: (String) -> Unit,
) { ) {
TextField( TextField(
@ -22,11 +19,9 @@ fun BlackTextField(
value = value, value = value,
onValueChange = onValueChange, onValueChange = onValueChange,
keyboardOptions = keyboardOptions, keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
placeholder = { placeholder = {
Text(text = placeHolder) Text(text = placeHolder)
}, },
singleLine = singleLine,
shape = RoundedShape(), shape = RoundedShape(),
colors = Colors.blackTextField, colors = Colors.blackTextField,
) )

View file

@ -35,10 +35,9 @@ data class FilterByQuery<Value>(
override fun invoke(value: Value): Boolean { override fun invoke(value: Value): Boolean {
if (query.isEmpty()) return onEmpty if (query.isEmpty()) return onEmpty
val transformed = transform(value) val transformed = transform(value)
val matchAgainst = if (ignoreCase) transformed.lowercase() else transformed return Levenshtein.ratio(
return Levenshtein.partialRatio(
query, query,
matchAgainst if (ignoreCase) transformed.lowercase() else transformed
) > ratio ) > ratio
} }
@ -69,30 +68,6 @@ data class FilterByQuery<Value>(
} }
} }
/** Sorting by closest match first */
@Immutable
data class SortByMatching<Value>(
val query: String,
val ignoreCase: Boolean = true,
val transform: (Value) -> String,
) : Comparator<Value> {
override fun compare(p0: Value?, p1: Value?): Int {
if (p0 == p1) return 0
if (p0 == null) return -1
if (p1 == null) return 1
return score(p1).compareTo(score(p0))
}
fun score(p0: Value) : Int {
val transformed = transform(p0)
val matchAgainst = if (ignoreCase) transformed.lowercase() else transformed
return Levenshtein.partialRatio(
query,
matchAgainst
)
}
}
/** Default sorting by name (Ascending A->Z), use `rev()` if Z->A is desired */ /** Default sorting by name (Ascending A->Z), use `rev()` if Z->A is desired */
@Immutable @Immutable
data class SortByName<Value>( data class SortByName<Value>(
@ -147,8 +122,6 @@ fun <K, V> PersistentMap<K, V>.edit(key: K, updater: V.() -> V): PersistentMap<K
* 2. `updating` calls that need both the old and new value, but to store the derived state we need to * 2. `updating` calls that need both the old and new value, but to store the derived state we need to
* store the old and new values outside the `updating` call. * store the old and new values outside the `updating` call.
* 3. `updateState` that might be called twice due to a race * 3. `updateState` that might be called twice due to a race
*
* The derived state is calculated *before* the filtering.
* */ * */
interface DerivedState<Self : DerivedState<Self, Value>, Value> { interface DerivedState<Self : DerivedState<Self, Value>, Value> {
/** State = {} */ /** State = {} */
@ -164,9 +137,6 @@ interface DerivedState<Self : DerivedState<Self, Value>, Value> {
/** /**
* For state that updated a lot we might need to calculate some accumulation, then * For state that updated a lot we might need to calculate some accumulation, then
* this is used to more ergonomically and efficiently do a `.filter {}` or `.count {}`. * this is used to more ergonomically and efficiently do a `.filter {}` or `.count {}`.
*
* Note that the derived state is calculated *before* the filtering, and as such the filterBy has
* no effect on the state.
*/ */
@Immutable @Immutable
data class SearchableDataState<Key, Value, State : DerivedState<State, Value>>( data class SearchableDataState<Key, Value, State : DerivedState<State, Value>>(
@ -179,9 +149,6 @@ data class SearchableDataState<Key, Value, State : DerivedState<State, Value>>(
val filteredBy get() = internalData.filteredBy val filteredBy get() = internalData.filteredBy
val sortedBy get() = internalData.sortedBy val sortedBy get() = internalData.sortedBy
@CheckResult
operator fun get(key : Key?) : Value? = internalData[key]
@CheckResult @CheckResult
fun sortingBy(newSorting: Comparator<Value>): SearchableDataState<Key, Value, State> = fun sortingBy(newSorting: Comparator<Value>): SearchableDataState<Key, Value, State> =
copy(internalData = internalData.sortingBy(newSorting)) copy(internalData = internalData.sortingBy(newSorting))
@ -190,13 +157,6 @@ data class SearchableDataState<Key, Value, State : DerivedState<State, Value>>(
fun filteringBy(newFilter: ((Value) -> Boolean)?): SearchableDataState<Key, Value, State> = fun filteringBy(newFilter: ((Value) -> Boolean)?): SearchableDataState<Key, Value, State> =
copy(internalData = internalData.filteringBy(newFilter)) copy(internalData = internalData.filteringBy(newFilter))
@CheckResult
fun searchBy(
newSorting: Comparator<Value>,
newFilter: ((Value) -> Boolean)?
): SearchableDataState<Key, Value, State> =
copy(internalData = internalData.searchBy(newSorting = newSorting, newFilter = newFilter))
@CheckResult @CheckResult
fun updating( fun updating(
key: Key, key: Key,
@ -313,9 +273,6 @@ data class SearchableData<Key, Value>(
* */ * */
val sortedBy: Comparator<Value>, val sortedBy: Comparator<Value>,
) { ) {
@CheckResult
operator fun get(key : Key?) : Value? = key?.let(data::get)
@CheckResult @CheckResult
fun sortingBy(newSorting: Comparator<Value>): SearchableData<Key, Value> { fun sortingBy(newSorting: Comparator<Value>): SearchableData<Key, Value> {
if (newSorting == sortedBy) { if (newSorting == sortedBy) {
@ -337,28 +294,6 @@ data class SearchableData<Key, Value>(
return copy(filteredBy = newFilter, filtered = newFiltered, sorted = newSorted) return copy(filteredBy = newFilter, filtered = newFiltered, sorted = newSorted)
} }
@CheckResult
fun searchBy(
newSorting: Comparator<Value>,
newFilter: ((Value) -> Boolean)?
): SearchableData<Key, Value> {
if (newFilter == filteredBy && newSorting == sortedBy) {
return this
}
val newFiltered = if (newFilter == filteredBy) {
filtered
} else {
filter(data = data, filteredBy = newFilter)
}
val newSorted = sort(data = data, filtered = newFiltered, sortedBy = newSorting)
return copy(
filteredBy = newFilter,
sortedBy = newSorting,
filtered = newFiltered,
sorted = newSorted
)
}
@CheckResult @CheckResult
fun updating( fun updating(
key: Key, key: Key,
@ -406,7 +341,8 @@ data class SearchableData<Key, Value>(
} else if (newFiltered.size < filtered.size) { } else if (newFiltered.size < filtered.size) {
/** Shown -> Hidden, If the update filtered out the item then just remove it from the list */ /** Shown -> Hidden, If the update filtered out the item then just remove it from the list */
return copy(data = newData, filtered = newFiltered, sorted = sorted.removing(key)) return copy(data = newData, filtered = newFiltered, sorted = sorted.removing(key))
} else { } else
{
/** Hidden -> Shown, we only need to insert it to the sorted list */ /** Hidden -> Shown, we only need to insert it to the sorted list */
return copy( return copy(
data = newData, filtered = newFiltered, sorted = addItemAt( data = newData, filtered = newFiltered, sorted = addItemAt(

View file

@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
@ -20,7 +21,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.lagradost.cloudstream4.compose.WhiteIconButton
import com.lagradost.cloudstream4.generated.resources.Res import com.lagradost.cloudstream4.generated.resources.Res
import com.lagradost.cloudstream4.generated.resources.action_bar_up_description import com.lagradost.cloudstream4.generated.resources.action_bar_up_description
import com.lagradost.cloudstream4.generated.resources.arrow_back import com.lagradost.cloudstream4.generated.resources.arrow_back
@ -104,7 +104,7 @@ fun AppBar(
windowInsets = WindowInsets(0, 0, 0, 0), windowInsets = WindowInsets(0, 0, 0, 0),
navigationIcon = { navigationIcon = {
if (isActionMode) { if (isActionMode) {
WhiteIconButton(onClick = onCancelActionMode) { IconButton(onClick = onCancelActionMode) {
Icon( Icon(
painter = painterResource(Res.drawable.close), painter = painterResource(Res.drawable.close),
contentDescription = stringResource(Res.string.cancel), contentDescription = stringResource(Res.string.cancel),
@ -112,7 +112,7 @@ fun AppBar(
} }
} else { } else {
navigateUp?.let { navigateUp?.let {
WhiteIconButton(onClick = it) { IconButton(onClick = it) {
UpIcon(navigationIcon = navigationIcon) UpIcon(navigationIcon = navigationIcon)
} }
} }

View file

@ -8,7 +8,6 @@ import androidx.compose.animation.core.repeatable
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
@ -37,13 +36,12 @@ import kotlinx.coroutines.delay
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@Composable @Composable
fun BasePreferenceWidget( internal fun BasePreferenceWidget(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
title: String? = null, title: String? = null,
subcomponent: @Composable (ColumnScope.() -> Unit)? = null, subcomponent: @Composable (ColumnScope.() -> Unit)? = null,
icon: @Composable (() -> Unit)? = null, icon: @Composable (() -> Unit)? = null,
onClick: (() -> Unit)? = null, onClick: (() -> Unit)? = null,
onLongClick: (() -> Unit)? = null,
widget: @Composable (() -> Unit)? = null, widget: @Composable (() -> Unit)? = null,
) { ) {
val highlighted = LocalPreferenceHighlighted.current val highlighted = LocalPreferenceHighlighted.current
@ -53,7 +51,7 @@ fun BasePreferenceWidget(
.highlightBackground(highlighted) .highlightBackground(highlighted)
.focusOutline() .focusOutline()
.sizeIn(minHeight = minHeight) .sizeIn(minHeight = minHeight)
.combinedClickable(enabled = onClick != null, onLongClick = onLongClick, onClick = { onClick?.invoke() }) .clickable(enabled = onClick != null, onClick = { onClick?.invoke() })
.fillMaxWidth(), .fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
@ -92,13 +90,11 @@ fun BasePreferenceWidget(
@Composable @Composable
internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier { internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier {
var highlightFlag by remember { mutableStateOf(false) } var highlightFlag by remember { mutableStateOf(false) }
LaunchedEffect(highlighted) { LaunchedEffect(Unit) {
if (highlighted) { if (highlighted) {
highlightFlag = true highlightFlag = true
delay(3.seconds) delay(3.seconds)
highlightFlag = false highlightFlag = false
} else {
highlightFlag = false
} }
} }
val highlight by animateColorAsState( val highlight by animateColorAsState(
@ -126,6 +122,6 @@ internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier {
} }
internal val TrailingWidgetBuffer = 16.dp internal val TrailingWidgetBuffer = 16.dp
val PrefsHorizontalPadding = 16.dp internal val PrefsHorizontalPadding = 16.dp
internal val PrefsVerticalPadding = 16.dp internal val PrefsVerticalPadding = 16.dp
internal val TitleFontSize = 16.sp internal val TitleFontSize = 16.sp

View file

@ -1,26 +0,0 @@
package com.lagradost.cloudstream4
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.Clipboard
import androidx.compose.ui.platform.asAwtTransferable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.awt.datatransfer.DataFlavor
import java.awt.datatransfer.StringSelection
@OptIn(ExperimentalComposeUiApi::class)
actual suspend fun Clipboard.getPlainText(): String? = withContext(Dispatchers.IO) {
val transferable = this@getPlainText.getClipEntry()?.asAwtTransferable
if (transferable != null && transferable.isDataFlavorSupported(DataFlavor.stringFlavor))
transferable.getTransferData(DataFlavor.stringFlavor) as? String
else null
}
@OptIn(ExperimentalComposeUiApi::class)
actual suspend fun Clipboard.setPlainText(
label: String,
text: String
) {
this.setClipEntry(ClipEntry(StringSelection(text)))
}