mirror of
https://github.com/recloudstream/cloudstream.git
synced 2026-10-22 18:14:16 +00:00
Compare commits
5 commits
fix-downlo
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad44a64f0c |
||
|
|
dc9d1f0f9e |
||
|
|
eb962b4fb7 |
||
|
|
4c4c6796ff |
||
|
|
5cddd5b364 |
44 changed files with 3797 additions and 67 deletions
|
|
@ -52,6 +52,7 @@ 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
|
||||||
|
|
@ -266,7 +267,9 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -590,7 +590,7 @@ object PluginManager {
|
||||||
/**
|
/**
|
||||||
* @return True if successful, false if not
|
* @return True if successful, false if not
|
||||||
* */
|
* */
|
||||||
private suspend fun loadPlugin(context: Context, file: File, data: PluginData): Boolean {
|
internal 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
|
||||||
|
|
|
||||||
|
|
@ -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 = 5, cacheUnit = TimeUnit.MINUTES)
|
app.get(convertRawGitUrl(url), cacheTime = 15, 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 = 5, cacheUnit = TimeUnit.MINUTES)
|
app.get(convertRawGitUrl(pluginUrls), cacheTime = 15, cacheUnit = TimeUnit.MINUTES)
|
||||||
.parsed<Array<SitePlugin>>().toList()
|
.parsed<Array<SitePlugin>>().toList()
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
logError(t)
|
logError(t)
|
||||||
|
|
@ -247,9 +247,16 @@ object RepositoryManager {
|
||||||
private val repoLock = Mutex()
|
private val repoLock = Mutex()
|
||||||
suspend fun addRepository(repository: RepositoryData) {
|
suspend fun addRepository(repository: RepositoryData) {
|
||||||
repoLock.withLock {
|
repoLock.withLock {
|
||||||
val currentRepos = getRepositories()
|
// No duplicates, but we want the new repository instead of the old one
|
||||||
// No duplicates
|
var currentRepos = getRepositories()
|
||||||
setKey(REPOSITORIES_KEY, (currentRepos + repository).distinctBy { it.url })
|
val index = currentRepos.indexOfFirst { it.url == repository.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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,7 @@ object AccountScreen {
|
||||||
.padding(contentPadding),
|
.padding(contentPadding),
|
||||||
horizontalAlignment = Alignment.End
|
horizontalAlignment = Alignment.End
|
||||||
) {
|
) {
|
||||||
val (edit, accounts) = FocusRequester.createRefs()
|
val (edit, accounts) = remember { FocusRequester.createRefs() }
|
||||||
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
|
|
@ -318,7 +318,7 @@ object AccountScreen {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val (focusRequester) = FocusRequester.createRefs()
|
val focusRequester = remember { FocusRequester() }
|
||||||
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) = FocusRequester.createRefs()
|
val focusRequester = remember { FocusRequester() }
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (selected) {
|
if (selected) {
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
|
|
|
||||||
|
|
@ -887,7 +887,6 @@ 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
|
||||||
|
|
@ -906,7 +905,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 = prevCenterMenuGone
|
playerView.playerCenterMenu?.isGone = true
|
||||||
playerView.playerVideoHolder?.isVisible = prevVideoHolderVisible
|
playerView.playerVideoHolder?.isVisible = prevVideoHolderVisible
|
||||||
rewHolder.alpha = 0f
|
rewHolder.alpha = 0f
|
||||||
}
|
}
|
||||||
|
|
@ -927,7 +926,6 @@ 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
|
||||||
|
|
@ -945,7 +943,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 = prevCenterMenuGone
|
playerView.playerCenterMenu?.isGone = true
|
||||||
playerView.playerVideoHolder?.isVisible = prevVideoHolderVisible
|
playerView.playerVideoHolder?.isVisible = prevVideoHolderVisible
|
||||||
ffwdHolder.alpha = 0f
|
ffwdHolder.alpha = 0f
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -189,6 +189,58 @@ 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,
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ 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
|
||||||
|
|
@ -30,7 +31,6 @@ 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,6 +47,11 @@ 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
|
||||||
|
|
@ -290,23 +295,52 @@ 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
|
/**
|
||||||
// showKeyboardOnFocus = false
|
* We do not want to pull up the keyboard when navigating to the item on TV, therefore
|
||||||
|
* 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 = isLayout(TV), CircleShape)
|
.focusOutline(enabled = isTv, CircleShape)
|
||||||
.onFocusChanged { newFocus ->
|
.onFocusChanged { newFocus ->
|
||||||
hasFocus = newFocus.hasFocus
|
hasFocus = newFocus.hasFocus
|
||||||
}.focusRequester(focusRequester),
|
/** Loss of focus -> readonly */
|
||||||
|
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))
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -13,17 +13,15 @@ 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.plugins.PluginManager
|
import com.lagradost.cloudstream3.services.BackupWorkManager
|
||||||
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
|
||||||
|
|
@ -147,7 +145,14 @@ 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),
|
||||||
|
|
@ -178,7 +183,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),
|
||||||
|
|
@ -197,7 +202,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),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,282 @@
|
||||||
|
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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
val newRepo = RepositoryData(repository.iconUrl, fixedName, url, repository.description)
|
||||||
RepositoryManager.addRepository(newRepo)
|
RepositoryManager.addRepository(newRepo)
|
||||||
extensionViewModel.loadStats()
|
extensionViewModel.loadStats()
|
||||||
extensionViewModel.loadRepositories()
|
extensionViewModel.loadRepositories()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
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
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -23,8 +23,21 @@ 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(null, name, url)
|
constructor(name: String, url: String) : this(
|
||||||
|
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"
|
||||||
|
|
|
||||||
|
|
@ -527,7 +527,8 @@ 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 {
|
||||||
|
|
|
||||||
|
|
@ -1045,7 +1045,7 @@ object VideoDownloadManager {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// too short file, treat it as an invalid link
|
// too short file, treat it as a 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,15 +1241,13 @@ 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 usually happens when storage access is revoked
|
// some sort of IO error, this should not happened
|
||||||
// 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
|
||||||
|
|
@ -1820,9 +1818,6 @@ 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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -215,9 +215,7 @@ object DownloadObjects {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun delete(): Boolean {
|
fun delete(): Boolean {
|
||||||
return runCatching {
|
return file.delete() == true
|
||||||
file.delete()
|
|
||||||
}.getOrNull() == true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val resume: Boolean get() = fileLength > 0L
|
val resume: Boolean get() = fileLength > 0L
|
||||||
|
|
|
||||||
|
|
@ -32,10 +32,10 @@ class TheIntroDBSkip : SkipAPI() {
|
||||||
?: return null
|
?: return null
|
||||||
|
|
||||||
val url = if (data.isMovie()) {
|
val url = if (data.isMovie()) {
|
||||||
"$mainUrl/v2/media?$idSuffix"
|
"$mainUrl/v4/media?$idSuffix"
|
||||||
} else {
|
} else {
|
||||||
val season = episode.season ?: return null
|
val season = episode.season ?: return null
|
||||||
"$mainUrl/v2/media?$idSuffix&season=$season&episode=${episode.episode}"
|
"$mainUrl/v4/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
|
||||||
).map { (list, type) ->
|
).flatMap { (list, type) ->
|
||||||
list.map { stamp ->
|
list.map { stamp ->
|
||||||
SkipStamp(
|
SkipStamp(
|
||||||
type,
|
type,
|
||||||
|
|
@ -51,12 +51,11 @@ 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(),
|
||||||
|
|
|
||||||
10
app/src/main/res/drawable/arrow_circle_down_24px.xml
Normal file
10
app/src/main/res/drawable/arrow_circle_down_24px.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<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>
|
||||||
51
app/src/main/res/drawable/codeberg.xml
Normal file
51
app/src/main/res/drawable/codeberg.xml
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
<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>
|
||||||
10
app/src/main/res/drawable/delete_sweep_24px.xml
Normal file
10
app/src/main/res/drawable/delete_sweep_24px.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<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>
|
||||||
10
app/src/main/res/drawable/disabled_by_default_24px.xml
Normal file
10
app/src/main/res/drawable/disabled_by_default_24px.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<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>
|
||||||
10
app/src/main/res/drawable/error_24px.xml
Normal file
10
app/src/main/res/drawable/error_24px.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<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>
|
||||||
10
app/src/main/res/drawable/extension_off_24px.xml
Normal file
10
app/src/main/res/drawable/extension_off_24px.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<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>
|
||||||
5
app/src/main/res/drawable/pastebin.xml
Normal file
5
app/src/main/res/drawable/pastebin.xml
Normal file
File diff suppressed because one or more lines are too long
10
app/src/main/res/drawable/volunteer_activism_24px.xml
Normal file
10
app/src/main/res/drawable/volunteer_activism_24px.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<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>
|
||||||
10
app/src/main/res/drawable/warning_24px.xml
Normal file
10
app/src/main/res/drawable/warning_24px.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<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>
|
||||||
|
|
@ -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.ExtensionsFragment"
|
android:name="com.lagradost.cloudstream3.ui.settings.extensions.ExtensionsFragment2"
|
||||||
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"
|
||||||
|
|
|
||||||
|
|
@ -83,9 +83,6 @@
|
||||||
<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>
|
||||||
|
|
@ -816,4 +813,5 @@
|
||||||
<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>
|
||||||
|
|
|
||||||
|
|
@ -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 { "\n$it" } ?: "" else ""
|
val prefix = if (showMessage) this.message?.let { "$it\n" } ?: "" else ""
|
||||||
return prefix + this.stackTraceToString()
|
return prefix + this.stackTraceToString()
|
||||||
.lines()
|
.lines()
|
||||||
.mapNotNull { line ->
|
.mapNotNull { line ->
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
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?
|
||||||
|
|
@ -34,6 +34,7 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,15 @@ 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
|
||||||
|
|
@ -15,6 +20,7 @@ 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
|
||||||
|
|
@ -49,6 +55,26 @@ 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,
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ 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,
|
||||||
) {
|
) {
|
||||||
|
|
@ -31,16 +32,24 @@ fun WhiteFilterChip(
|
||||||
border = FilterChipDefaults.filterChipBorder(
|
border = FilterChipDefaults.filterChipBorder(
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
selected = selected,
|
selected = selected,
|
||||||
borderColor = MaterialTheme.colorScheme.surfaceVariant,
|
borderColor = if (hasFocus) {
|
||||||
|
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 = Colors.whiteChip,
|
colors = if (hasFocus) {
|
||||||
|
Colors.whiteChipFocused
|
||||||
|
} else {
|
||||||
|
Colors.whiteChip
|
||||||
|
},
|
||||||
label = {
|
label = {
|
||||||
Text(label)
|
Text(label)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,24 @@ 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,
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
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
|
||||||
|
|
@ -63,7 +67,9 @@ 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)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
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
|
||||||
|
|
@ -12,6 +13,8 @@ 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(
|
||||||
|
|
@ -19,9 +22,11 @@ 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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -35,9 +35,10 @@ 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)
|
||||||
return Levenshtein.ratio(
|
val matchAgainst = if (ignoreCase) transformed.lowercase() else transformed
|
||||||
|
return Levenshtein.partialRatio(
|
||||||
query,
|
query,
|
||||||
if (ignoreCase) transformed.lowercase() else transformed
|
matchAgainst
|
||||||
) > ratio
|
) > ratio
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,6 +69,30 @@ 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>(
|
||||||
|
|
@ -122,6 +147,8 @@ 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 = {} */
|
||||||
|
|
@ -137,6 +164,9 @@ 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>>(
|
||||||
|
|
@ -149,6 +179,9 @@ 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))
|
||||||
|
|
@ -157,6 +190,13 @@ 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,
|
||||||
|
|
@ -273,6 +313,9 @@ 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) {
|
||||||
|
|
@ -294,6 +337,28 @@ 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,
|
||||||
|
|
@ -341,8 +406,7 @@ 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(
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -5,7 +5,6 @@ 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
|
||||||
|
|
@ -21,6 +20,7 @@ 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) {
|
||||||
IconButton(onClick = onCancelActionMode) {
|
WhiteIconButton(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 {
|
||||||
IconButton(onClick = it) {
|
WhiteIconButton(onClick = it) {
|
||||||
UpIcon(navigationIcon = navigationIcon)
|
UpIcon(navigationIcon = navigationIcon)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ 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
|
||||||
|
|
@ -36,12 +37,13 @@ import kotlinx.coroutines.delay
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun BasePreferenceWidget(
|
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
|
||||||
|
|
@ -51,7 +53,7 @@ internal fun BasePreferenceWidget(
|
||||||
.highlightBackground(highlighted)
|
.highlightBackground(highlighted)
|
||||||
.focusOutline()
|
.focusOutline()
|
||||||
.sizeIn(minHeight = minHeight)
|
.sizeIn(minHeight = minHeight)
|
||||||
.clickable(enabled = onClick != null, onClick = { onClick?.invoke() })
|
.combinedClickable(enabled = onClick != null, onLongClick = onLongClick, onClick = { onClick?.invoke() })
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
|
|
@ -90,11 +92,13 @@ internal 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(Unit) {
|
LaunchedEffect(highlighted) {
|
||||||
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(
|
||||||
|
|
@ -122,6 +126,6 @@ internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
internal val TrailingWidgetBuffer = 16.dp
|
internal val TrailingWidgetBuffer = 16.dp
|
||||||
internal val PrefsHorizontalPadding = 16.dp
|
val PrefsHorizontalPadding = 16.dp
|
||||||
internal val PrefsVerticalPadding = 16.dp
|
internal val PrefsVerticalPadding = 16.dp
|
||||||
internal val TitleFontSize = 16.sp
|
internal val TitleFontSize = 16.sp
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
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)))
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue