mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-29 23:59:32 +00:00
ref(cloud): refactor badge handling
This commit is contained in:
parent
34b8c757ac
commit
32f99fc252
9 changed files with 230 additions and 190 deletions
|
|
@ -17,7 +17,7 @@ data class DebridSettings(
|
|||
val streamPreferences: DebridStreamPreferences = DebridStreamPreferences(),
|
||||
val streamNameTemplate: String = DebridStreamFormatterDefaults.NAME_TEMPLATE,
|
||||
val streamDescriptionTemplate: String = DebridStreamFormatterDefaults.DESCRIPTION_TEMPLATE,
|
||||
val streamBadgeRules: DebridStreamBadgeRules = DebridStreamBadgeRules(),
|
||||
val streamBadgeRules: StreamBadgeRules = StreamBadgeRules(),
|
||||
) {
|
||||
val torboxApiKey: String
|
||||
get() = apiKeyFor(DebridProviders.TORBOX_ID)
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ object DebridSettingsRepository {
|
|||
private var streamPreferences = DebridStreamPreferences()
|
||||
private var streamNameTemplate = DebridStreamFormatterDefaults.NAME_TEMPLATE
|
||||
private var streamDescriptionTemplate = DebridStreamFormatterDefaults.DESCRIPTION_TEMPLATE
|
||||
private var streamBadgeRules = DebridStreamBadgeRules()
|
||||
private var streamBadgeRules = StreamBadgeRules()
|
||||
|
||||
fun ensureLoaded() {
|
||||
if (hasLoaded) return
|
||||
|
|
@ -230,16 +230,16 @@ object DebridSettingsRepository {
|
|||
)
|
||||
}
|
||||
|
||||
suspend fun importStreamBadgeRulesFromUrl(url: String): DebridStreamBadgeImportResult {
|
||||
suspend fun importStreamBadgeRulesFromUrl(url: String): StreamBadgeImportResult {
|
||||
ensureLoaded()
|
||||
val normalizedUrl = url.trim()
|
||||
if (normalizedUrl.isBlank()) {
|
||||
return DebridStreamBadgeImportResult.Error("Enter a badge JSON URL.")
|
||||
return StreamBadgeImportResult.Error("Enter a badge JSON URL.")
|
||||
}
|
||||
if (!normalizedUrl.startsWith("https://", ignoreCase = true) &&
|
||||
!normalizedUrl.startsWith("http://", ignoreCase = true)
|
||||
) {
|
||||
return DebridStreamBadgeImportResult.Error("Badge URL must start with http:// or https://.")
|
||||
return StreamBadgeImportResult.Error("Badge URL must start with http:// or https://.")
|
||||
}
|
||||
|
||||
return try {
|
||||
|
|
@ -247,21 +247,21 @@ object DebridSettingsRepository {
|
|||
val isExistingImport = currentRules.imports.any { import ->
|
||||
import.sourceUrl.equals(normalizedUrl, ignoreCase = true)
|
||||
}
|
||||
if (!isExistingImport && currentRules.imports.size >= DEBRID_STREAM_BADGE_IMPORT_LIMIT) {
|
||||
return DebridStreamBadgeImportResult.Error("You can import up to $DEBRID_STREAM_BADGE_IMPORT_LIMIT badge URLs.")
|
||||
if (!isExistingImport && currentRules.imports.size >= STREAM_BADGE_IMPORT_LIMIT) {
|
||||
return StreamBadgeImportResult.Error("You can import up to $STREAM_BADGE_IMPORT_LIMIT badge URLs.")
|
||||
}
|
||||
val payload = httpGetText(normalizedUrl)
|
||||
val parsedImport = DebridStreamBadgeRulesParser.parse(
|
||||
val parsedImport = StreamBadgeRulesParser.parse(
|
||||
sourceUrl = normalizedUrl,
|
||||
payload = payload,
|
||||
)
|
||||
streamBadgeRules = currentRules.upsert(parsedImport, activate = true)
|
||||
publish()
|
||||
saveStreamBadgeRules()
|
||||
DebridStreamBadgeImportResult.Success(streamBadgeRules)
|
||||
StreamBadgeImportResult.Success(streamBadgeRules)
|
||||
} catch (error: Exception) {
|
||||
if (error is CancellationException) throw error
|
||||
DebridStreamBadgeImportResult.Error(error.message ?: "Badge import failed.")
|
||||
StreamBadgeImportResult.Error(error.message ?: "Badge import failed.")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -383,7 +383,7 @@ object DebridSettingsRepository {
|
|||
DebridSettingsStorage.loadStreamDescriptionTemplate().orEmpty(),
|
||||
DebridTemplateKind.DESCRIPTION,
|
||||
)
|
||||
streamBadgeRules = parseStreamBadgeRules(DebridSettingsStorage.loadStreamBadgeRules()) ?: DebridStreamBadgeRules()
|
||||
streamBadgeRules = parseStreamBadgeRules(DebridSettingsStorage.loadStreamBadgeRules()) ?: StreamBadgeRules()
|
||||
publish()
|
||||
}
|
||||
|
||||
|
|
@ -435,10 +435,10 @@ object DebridSettingsRepository {
|
|||
}
|
||||
}
|
||||
|
||||
private fun parseStreamBadgeRules(value: String?): DebridStreamBadgeRules? {
|
||||
private fun parseStreamBadgeRules(value: String?): StreamBadgeRules? {
|
||||
if (value.isNullOrBlank()) return null
|
||||
val decodedRules = try {
|
||||
json.decodeFromString<DebridStreamBadgeRules>(value).normalized()
|
||||
json.decodeFromString<StreamBadgeRules>(value).normalized()
|
||||
} catch (_: SerializationException) {
|
||||
null
|
||||
} catch (_: IllegalArgumentException) {
|
||||
|
|
@ -447,7 +447,7 @@ object DebridSettingsRepository {
|
|||
if (decodedRules?.hasImport == true) return decodedRules
|
||||
|
||||
val legacyRules = try {
|
||||
json.decodeFromString<LegacyDebridStreamBadgeRules>(value)
|
||||
json.decodeFromString<LegacyStreamBadgeRules>(value)
|
||||
.toBadgeRules()
|
||||
.normalized()
|
||||
} catch (_: SerializationException) {
|
||||
|
|
@ -475,15 +475,15 @@ object DebridSettingsRepository {
|
|||
}
|
||||
|
||||
@Serializable
|
||||
private data class LegacyDebridStreamBadgeRules(
|
||||
private data class LegacyStreamBadgeRules(
|
||||
val sourceUrl: String = "",
|
||||
val filters: List<DebridStreamBadgeFilter> = emptyList(),
|
||||
val groups: List<DebridStreamBadgeGroup> = emptyList(),
|
||||
val filters: List<StreamBadgeFilter> = emptyList(),
|
||||
val groups: List<StreamBadgeGroup> = emptyList(),
|
||||
) {
|
||||
fun toBadgeRules(): DebridStreamBadgeRules =
|
||||
DebridStreamBadgeRules(
|
||||
fun toBadgeRules(): StreamBadgeRules =
|
||||
StreamBadgeRules(
|
||||
imports = listOf(
|
||||
DebridStreamBadgeImport(
|
||||
StreamBadgeImport(
|
||||
sourceUrl = sourceUrl,
|
||||
filters = filters,
|
||||
groups = groups,
|
||||
|
|
|
|||
|
|
@ -13,11 +13,11 @@ class DebridStreamFormatter(
|
|||
fun format(
|
||||
stream: StreamItem,
|
||||
settings: DebridSettings,
|
||||
compiledBadgeFilters: List<DebridCompiledStreamBadgeFilter> =
|
||||
DebridStreamBadgeMatcher.compile(settings.streamBadgeRules),
|
||||
compiledBadgeFilters: List<CompiledStreamBadgeFilter> =
|
||||
StreamBadgeMatcher.compile(settings.streamBadgeRules),
|
||||
): StreamItem {
|
||||
if (!stream.isManagedDebridStream) return stream
|
||||
val matchedBadges = DebridStreamBadgeMatcher.matchedBadges(stream, compiledBadgeFilters)
|
||||
val matchedBadges = StreamBadgeMatcher.matchedBadges(stream, compiledBadgeFilters)
|
||||
val values = buildValues(stream, settings, matchedBadges)
|
||||
val nameTemplate = settings.streamNameTemplate.ifBlank { DebridStreamFormatterDefaults.NAME_TEMPLATE }
|
||||
val descriptionTemplate = settings.streamDescriptionTemplate.ifBlank { DebridStreamFormatterDefaults.DESCRIPTION_TEMPLATE }
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ object DebridStreamPresentation {
|
|||
val debridStreams = visibleStreams.filter { stream -> stream.isManagedDebridStream }
|
||||
if (debridStreams.isEmpty()) return@map group.copy(streams = visibleStreams)
|
||||
|
||||
val compiledBadgeFilters = DebridStreamBadgeMatcher.compile(settings.streamBadgeRules)
|
||||
val compiledBadgeFilters = StreamBadgeMatcher.compile(settings.streamBadgeRules)
|
||||
val shouldFormatStreams = settings.hasCustomStreamFormatting || compiledBadgeFilters.isNotEmpty()
|
||||
val presentedDebridStreams = applyPreferences(debridStreams, settings)
|
||||
.map { stream ->
|
||||
|
|
|
|||
|
|
@ -0,0 +1,102 @@
|
|||
package com.nuvio.app.features.debrid
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil3.compose.AsyncImage
|
||||
|
||||
internal object BadgeChipDefaults {
|
||||
val shape = RoundedCornerShape(6.dp)
|
||||
val fileSizeHorizontalPadding = 6.dp
|
||||
val fileSizeFontSize: TextUnit = 10.sp
|
||||
val fileSizeLineHeight: TextUnit = 12.sp
|
||||
val fileSizeLetterSpacing: TextUnit = 0.sp
|
||||
}
|
||||
|
||||
internal enum class ImportedBadgeChipSize(
|
||||
val containerHeight: Dp,
|
||||
val imageHeight: Dp,
|
||||
val minImageWidth: Dp,
|
||||
val maxImageWidth: Dp,
|
||||
val horizontalPadding: Dp,
|
||||
val verticalPadding: Dp,
|
||||
) {
|
||||
STREAM(
|
||||
containerHeight = 20.dp,
|
||||
imageHeight = 16.dp,
|
||||
minImageWidth = 34.dp,
|
||||
maxImageWidth = 92.dp,
|
||||
horizontalPadding = 3.dp,
|
||||
verticalPadding = 2.dp,
|
||||
),
|
||||
PREVIEW(
|
||||
containerHeight = 24.dp,
|
||||
imageHeight = 18.dp,
|
||||
minImageWidth = 38.dp,
|
||||
maxImageWidth = 112.dp,
|
||||
horizontalPadding = 4.dp,
|
||||
verticalPadding = 3.dp,
|
||||
),
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun ImportedBadgeChip(
|
||||
imageURL: String,
|
||||
name: String,
|
||||
tagColor: String,
|
||||
tagStyle: String,
|
||||
borderColor: String,
|
||||
size: ImportedBadgeChipSize,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val backgroundColor = if (tagStyle.equals("filled", ignoreCase = true)) {
|
||||
tagColor.toBadgeColorOrNull()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val outlineColor = borderColor.toBadgeColorOrNull()
|
||||
val shape = BadgeChipDefaults.shape
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.height(size.containerHeight)
|
||||
.then(if (backgroundColor != null) Modifier.background(backgroundColor, shape) else Modifier)
|
||||
.then(if (outlineColor != null) Modifier.border(1.dp, outlineColor, shape) else Modifier)
|
||||
.padding(horizontal = size.horizontalPadding, vertical = size.verticalPadding),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
AsyncImage(
|
||||
model = imageURL,
|
||||
contentDescription = name,
|
||||
modifier = Modifier
|
||||
.height(size.imageHeight)
|
||||
.widthIn(min = size.minImageWidth, max = size.maxImageWidth)
|
||||
.clip(shape),
|
||||
contentScale = ContentScale.Fit,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.toBadgeColorOrNull(): Color? {
|
||||
val hex = trim().removePrefix("#")
|
||||
val argb = when (hex.length) {
|
||||
6 -> "FF$hex"
|
||||
8 -> hex
|
||||
else -> return null
|
||||
}
|
||||
return argb.toLongOrNull(16)?.let { Color(it) }
|
||||
}
|
||||
|
|
@ -8,23 +8,23 @@ import kotlinx.serialization.SerializationException
|
|||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
const val DEBRID_STREAM_BADGE_IMPORT_LIMIT = 3
|
||||
const val STREAM_BADGE_IMPORT_LIMIT = 3
|
||||
|
||||
@Serializable
|
||||
data class DebridStreamBadgeRules(
|
||||
val imports: List<DebridStreamBadgeImport> = emptyList(),
|
||||
data class StreamBadgeRules(
|
||||
val imports: List<StreamBadgeImport> = emptyList(),
|
||||
) {
|
||||
val hasImport: Boolean
|
||||
get() = imports.isNotEmpty()
|
||||
|
||||
val activeImport: DebridStreamBadgeImport?
|
||||
val activeImport: StreamBadgeImport?
|
||||
get() = imports.firstOrNull { it.isActive } ?: imports.firstOrNull()
|
||||
|
||||
val enabledFilterCount: Int
|
||||
get() = activeImport?.enabledFilterCount ?: 0
|
||||
|
||||
fun normalized(): DebridStreamBadgeRules {
|
||||
val normalizedImports = mutableListOf<DebridStreamBadgeImport>()
|
||||
fun normalized(): StreamBadgeRules {
|
||||
val normalizedImports = mutableListOf<StreamBadgeImport>()
|
||||
imports.forEach { import ->
|
||||
val normalizedUrl = import.sourceUrl.trim()
|
||||
if (normalizedUrl.isBlank() || import.filters.isEmpty()) return@forEach
|
||||
|
|
@ -32,7 +32,7 @@ data class DebridStreamBadgeRules(
|
|||
val existingIndex = normalizedImports.indexOfFirst { it.sourceUrl.equals(normalizedUrl, ignoreCase = true) }
|
||||
if (existingIndex >= 0) {
|
||||
normalizedImports[existingIndex] = normalizedImport
|
||||
} else if (normalizedImports.size < DEBRID_STREAM_BADGE_IMPORT_LIMIT) {
|
||||
} else if (normalizedImports.size < STREAM_BADGE_IMPORT_LIMIT) {
|
||||
normalizedImports += normalizedImport
|
||||
}
|
||||
}
|
||||
|
|
@ -45,7 +45,7 @@ data class DebridStreamBadgeRules(
|
|||
)
|
||||
}
|
||||
|
||||
fun upsert(import: DebridStreamBadgeImport, activate: Boolean = true): DebridStreamBadgeRules {
|
||||
fun upsert(import: StreamBadgeImport, activate: Boolean = true): StreamBadgeRules {
|
||||
val normalizedUrl = import.sourceUrl.trim()
|
||||
if (normalizedUrl.isBlank()) return normalized()
|
||||
val normalizedImport = import.copy(sourceUrl = normalizedUrl, isActive = activate)
|
||||
|
|
@ -67,7 +67,7 @@ data class DebridStreamBadgeRules(
|
|||
return copy(imports = activeImports).normalized()
|
||||
}
|
||||
|
||||
fun setActiveSource(sourceUrl: String): DebridStreamBadgeRules {
|
||||
fun setActiveSource(sourceUrl: String): StreamBadgeRules {
|
||||
val normalizedUrl = sourceUrl.trim()
|
||||
if (normalizedUrl.isBlank() || imports.none { it.sourceUrl.equals(normalizedUrl, ignoreCase = true) }) {
|
||||
return normalized()
|
||||
|
|
@ -79,29 +79,29 @@ data class DebridStreamBadgeRules(
|
|||
).normalized()
|
||||
}
|
||||
|
||||
fun removeSource(sourceUrl: String): DebridStreamBadgeRules =
|
||||
fun removeSource(sourceUrl: String): StreamBadgeRules =
|
||||
copy(imports = imports.filterNot { it.sourceUrl.equals(sourceUrl.trim(), ignoreCase = true) }).normalized()
|
||||
|
||||
override fun toString(): String =
|
||||
"DebridStreamBadgeRules(imports=${imports.size}, activeFilters=$enabledFilterCount, groups=${imports.sumOf { it.groups.size }})"
|
||||
"StreamBadgeRules(imports=${imports.size}, activeFilters=$enabledFilterCount, groups=${imports.sumOf { it.groups.size }})"
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class DebridStreamBadgeImport(
|
||||
data class StreamBadgeImport(
|
||||
val sourceUrl: String = "",
|
||||
val filters: List<DebridStreamBadgeFilter> = emptyList(),
|
||||
val groups: List<DebridStreamBadgeGroup> = emptyList(),
|
||||
val filters: List<StreamBadgeFilter> = emptyList(),
|
||||
val groups: List<StreamBadgeGroup> = emptyList(),
|
||||
val isActive: Boolean = true,
|
||||
) {
|
||||
val enabledFilterCount: Int
|
||||
get() = filters.count { it.isEnabled }
|
||||
|
||||
override fun toString(): String =
|
||||
"DebridStreamBadgeImport(sourceUrl=$sourceUrl, filters=${filters.size}, groups=${groups.size}, isActive=$isActive)"
|
||||
"StreamBadgeImport(sourceUrl=$sourceUrl, filters=${filters.size}, groups=${groups.size}, isActive=$isActive)"
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class DebridStreamBadgeFilter(
|
||||
data class StreamBadgeFilter(
|
||||
val id: String = "",
|
||||
val groupId: String = "",
|
||||
val name: String = "",
|
||||
|
|
@ -115,28 +115,28 @@ data class DebridStreamBadgeFilter(
|
|||
)
|
||||
|
||||
@Serializable
|
||||
data class DebridStreamBadgeGroup(
|
||||
data class StreamBadgeGroup(
|
||||
val id: String = "",
|
||||
val name: String = "",
|
||||
val color: String = "",
|
||||
val isExpanded: Boolean = true,
|
||||
)
|
||||
|
||||
sealed interface DebridStreamBadgeImportResult {
|
||||
data class Success(val rules: DebridStreamBadgeRules) : DebridStreamBadgeImportResult
|
||||
data class Error(val message: String) : DebridStreamBadgeImportResult
|
||||
sealed interface StreamBadgeImportResult {
|
||||
data class Success(val rules: StreamBadgeRules) : StreamBadgeImportResult
|
||||
data class Error(val message: String) : StreamBadgeImportResult
|
||||
}
|
||||
|
||||
internal object DebridStreamBadgeRulesParser {
|
||||
internal object StreamBadgeRulesParser {
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
explicitNulls = false
|
||||
}
|
||||
|
||||
fun parse(sourceUrl: String, payload: String): DebridStreamBadgeImport {
|
||||
fun parse(sourceUrl: String, payload: String): StreamBadgeImport {
|
||||
val decoded = try {
|
||||
json.decodeFromString<DebridStreamBadgePayload>(payload)
|
||||
json.decodeFromString<StreamBadgePayload>(payload)
|
||||
} catch (error: SerializationException) {
|
||||
throw IllegalArgumentException("Invalid badge JSON: ${error.message.orEmpty()}")
|
||||
} catch (error: IllegalArgumentException) {
|
||||
|
|
@ -150,7 +150,7 @@ internal object DebridStreamBadgeRulesParser {
|
|||
return@mapNotNull null
|
||||
}
|
||||
|
||||
DebridStreamBadgeFilter(
|
||||
StreamBadgeFilter(
|
||||
id = filter.id.orEmpty(),
|
||||
groupId = filter.groupId.orEmpty(),
|
||||
name = name,
|
||||
|
|
@ -169,7 +169,7 @@ internal object DebridStreamBadgeRulesParser {
|
|||
}
|
||||
|
||||
val groups = decoded.groups.map { group ->
|
||||
DebridStreamBadgeGroup(
|
||||
StreamBadgeGroup(
|
||||
id = group.id.orEmpty(),
|
||||
name = group.name.orEmpty(),
|
||||
color = group.color.orEmpty(),
|
||||
|
|
@ -177,7 +177,7 @@ internal object DebridStreamBadgeRulesParser {
|
|||
)
|
||||
}
|
||||
|
||||
return DebridStreamBadgeImport(
|
||||
return StreamBadgeImport(
|
||||
sourceUrl = sourceUrl.trim(),
|
||||
filters = filters,
|
||||
groups = groups,
|
||||
|
|
@ -185,8 +185,8 @@ internal object DebridStreamBadgeRulesParser {
|
|||
}
|
||||
}
|
||||
|
||||
internal object DebridStreamBadgeMatcher {
|
||||
fun compile(rules: DebridStreamBadgeRules): List<DebridCompiledStreamBadgeFilter> {
|
||||
internal object StreamBadgeMatcher {
|
||||
fun compile(rules: StreamBadgeRules): List<CompiledStreamBadgeFilter> {
|
||||
if (!rules.hasImport) return emptyList()
|
||||
return rules.normalized().imports.filter { it.isActive }.flatMap { import ->
|
||||
import.filters.mapNotNull { filter ->
|
||||
|
|
@ -194,7 +194,7 @@ internal object DebridStreamBadgeMatcher {
|
|||
return@mapNotNull null
|
||||
}
|
||||
val regex = runCatching { Regex(filter.pattern) }.getOrNull() ?: return@mapNotNull null
|
||||
DebridCompiledStreamBadgeFilter(
|
||||
CompiledStreamBadgeFilter(
|
||||
name = filter.name,
|
||||
badge = StreamBadge(
|
||||
name = filter.name,
|
||||
|
|
@ -210,14 +210,14 @@ internal object DebridStreamBadgeMatcher {
|
|||
}
|
||||
}
|
||||
|
||||
fun matchedNames(stream: StreamItem, rules: DebridStreamBadgeRules): List<String> =
|
||||
fun matchedNames(stream: StreamItem, rules: StreamBadgeRules): List<String> =
|
||||
matchedNames(stream, compile(rules))
|
||||
|
||||
fun matchedNames(stream: StreamItem, filters: List<DebridCompiledStreamBadgeFilter>): List<String> {
|
||||
fun matchedNames(stream: StreamItem, filters: List<CompiledStreamBadgeFilter>): List<String> {
|
||||
return matchedBadges(stream, filters).map { it.name }
|
||||
}
|
||||
|
||||
fun matchedBadges(stream: StreamItem, filters: List<DebridCompiledStreamBadgeFilter>): List<StreamBadge> {
|
||||
fun matchedBadges(stream: StreamItem, filters: List<CompiledStreamBadgeFilter>): List<StreamBadge> {
|
||||
if (filters.isEmpty()) return emptyList()
|
||||
val candidates = badgeMatchCandidates(stream)
|
||||
if (candidates.isEmpty()) return emptyList()
|
||||
|
|
@ -271,7 +271,7 @@ internal object DebridStreamBadgeMatcher {
|
|||
}
|
||||
}
|
||||
|
||||
data class DebridCompiledStreamBadgeFilter(
|
||||
data class CompiledStreamBadgeFilter(
|
||||
val name: String,
|
||||
val badge: StreamBadge,
|
||||
val regex: Regex,
|
||||
|
|
@ -281,13 +281,13 @@ private fun StreamBadge.dedupeKey(): String =
|
|||
imageURL.takeIf { it.isNotBlank() } ?: name
|
||||
|
||||
@Serializable
|
||||
private data class DebridStreamBadgePayload(
|
||||
val filters: List<DebridStreamBadgeFilterPayload> = emptyList(),
|
||||
val groups: List<DebridStreamBadgeGroupPayload> = emptyList(),
|
||||
private data class StreamBadgePayload(
|
||||
val filters: List<StreamBadgeFilterPayload> = emptyList(),
|
||||
val groups: List<StreamBadgeGroupPayload> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
private data class DebridStreamBadgeFilterPayload(
|
||||
private data class StreamBadgeFilterPayload(
|
||||
val id: String? = null,
|
||||
val groupId: String? = null,
|
||||
val name: String? = null,
|
||||
|
|
@ -302,7 +302,7 @@ private data class DebridStreamBadgeFilterPayload(
|
|||
)
|
||||
|
||||
@Serializable
|
||||
private data class DebridStreamBadgeGroupPayload(
|
||||
private data class StreamBadgeGroupPayload(
|
||||
val id: String? = null,
|
||||
val name: String? = null,
|
||||
val color: String? = null,
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
package com.nuvio.app.features.settings
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
|
|
@ -17,7 +15,6 @@ import androidx.compose.foundation.layout.heightIn
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.items
|
||||
|
|
@ -50,18 +47,16 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.features.debrid.DEBRID_PREPARE_INSTANT_PLAYBACK_DEFAULT_LIMIT
|
||||
import com.nuvio.app.features.debrid.DEBRID_STREAM_BADGE_IMPORT_LIMIT
|
||||
import com.nuvio.app.features.debrid.STREAM_BADGE_IMPORT_LIMIT
|
||||
import com.nuvio.app.features.debrid.ImportedBadgeChip
|
||||
import com.nuvio.app.features.debrid.ImportedBadgeChipSize
|
||||
import com.nuvio.app.features.debrid.DebridCredentialValidator
|
||||
import com.nuvio.app.features.debrid.DebridDeviceAuthorization
|
||||
import com.nuvio.app.features.debrid.DebridDeviceAuthorizationTokenResult
|
||||
|
|
@ -71,10 +66,10 @@ import com.nuvio.app.features.debrid.DebridProviderAuthMethod
|
|||
import com.nuvio.app.features.debrid.DebridProviders
|
||||
import com.nuvio.app.features.debrid.DebridSettings
|
||||
import com.nuvio.app.features.debrid.DebridSettingsRepository
|
||||
import com.nuvio.app.features.debrid.DebridStreamBadgeImportResult
|
||||
import com.nuvio.app.features.debrid.DebridStreamBadgeFilter
|
||||
import com.nuvio.app.features.debrid.DebridStreamBadgeImport
|
||||
import com.nuvio.app.features.debrid.DebridStreamBadgeRules
|
||||
import com.nuvio.app.features.debrid.StreamBadgeImportResult
|
||||
import com.nuvio.app.features.debrid.StreamBadgeFilter
|
||||
import com.nuvio.app.features.debrid.StreamBadgeImport
|
||||
import com.nuvio.app.features.debrid.StreamBadgeRules
|
||||
import com.nuvio.app.features.debrid.DebridStreamFormatterDefaults
|
||||
import com.nuvio.app.features.debrid.DebridStreamAudioChannel
|
||||
import com.nuvio.app.features.debrid.DebridStreamAudioTag
|
||||
|
|
@ -486,7 +481,7 @@ internal fun LazyListScope.debridSettingsContent(
|
|||
}
|
||||
|
||||
if (showBadgeImportDialog) {
|
||||
DebridBadgeUrlManagerDialog(
|
||||
BadgeUrlManagerDialog(
|
||||
currentRules = settings.streamBadgeRules,
|
||||
onDismiss = { showBadgeImportDialog = false },
|
||||
)
|
||||
|
|
@ -538,10 +533,10 @@ private fun templatePreview(value: String, defaultValue: String): String {
|
|||
return if (firstLine.length <= 28) firstLine else "${firstLine.take(28)}..."
|
||||
}
|
||||
|
||||
private fun badgeRulesPreview(rules: DebridStreamBadgeRules): String {
|
||||
private fun badgeRulesPreview(rules: StreamBadgeRules): String {
|
||||
val normalizedRules = rules.normalized()
|
||||
return if (normalizedRules.hasImport) {
|
||||
"${normalizedRules.imports.size}/$DEBRID_STREAM_BADGE_IMPORT_LIMIT URLs, ${normalizedRules.enabledFilterCount} active badges"
|
||||
"${normalizedRules.imports.size}/$STREAM_BADGE_IMPORT_LIMIT URLs, ${normalizedRules.enabledFilterCount} active badges"
|
||||
} else {
|
||||
"Not imported"
|
||||
}
|
||||
|
|
@ -712,8 +707,8 @@ private fun DebridTemplateDialog(
|
|||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
private fun DebridBadgeUrlManagerDialog(
|
||||
currentRules: DebridStreamBadgeRules,
|
||||
private fun BadgeUrlManagerDialog(
|
||||
currentRules: StreamBadgeRules,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
|
@ -721,12 +716,12 @@ private fun DebridBadgeUrlManagerDialog(
|
|||
var draftUrl by rememberSaveable { mutableStateOf("") }
|
||||
var errorMessage by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
var isImporting by rememberSaveable { mutableStateOf(false) }
|
||||
var previewImport by remember { mutableStateOf<DebridStreamBadgeImport?>(null) }
|
||||
var previewImport by remember { mutableStateOf<StreamBadgeImport?>(null) }
|
||||
|
||||
BasicAlertDialog(onDismissRequest = onDismiss) {
|
||||
DebridDialogSurface(title = "Badge URLs") {
|
||||
Text(
|
||||
text = "Import up to $DEBRID_STREAM_BADGE_IMPORT_LIMIT label badge JSON URLs. Each URL can be updated or deleted separately.",
|
||||
text = "Import up to $STREAM_BADGE_IMPORT_LIMIT label badge JSON URLs. Each URL can be updated or deleted separately.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
|
@ -756,7 +751,7 @@ private fun DebridBadgeUrlManagerDialog(
|
|||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = "${imports.size}/$DEBRID_STREAM_BADGE_IMPORT_LIMIT URLs imported",
|
||||
text = "${imports.size}/$STREAM_BADGE_IMPORT_LIMIT URLs imported",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.weight(1f),
|
||||
|
|
@ -768,11 +763,11 @@ private fun DebridBadgeUrlManagerDialog(
|
|||
isImporting = true
|
||||
errorMessage = null
|
||||
when (val result = DebridSettingsRepository.importStreamBadgeRulesFromUrl(draftUrl)) {
|
||||
is DebridStreamBadgeImportResult.Success -> {
|
||||
is StreamBadgeImportResult.Success -> {
|
||||
draftUrl = ""
|
||||
isImporting = false
|
||||
}
|
||||
is DebridStreamBadgeImportResult.Error -> {
|
||||
is StreamBadgeImportResult.Error -> {
|
||||
errorMessage = result.message
|
||||
isImporting = false
|
||||
}
|
||||
|
|
@ -816,7 +811,7 @@ private fun DebridBadgeUrlManagerDialog(
|
|||
items = imports,
|
||||
key = { import -> import.sourceUrl },
|
||||
) { import ->
|
||||
DebridBadgeUrlRow(
|
||||
BadgeUrlRow(
|
||||
import = import,
|
||||
showActiveChoice = imports.size > 1,
|
||||
enabled = !isImporting,
|
||||
|
|
@ -850,7 +845,7 @@ private fun DebridBadgeUrlManagerDialog(
|
|||
}
|
||||
|
||||
previewImport?.let { import ->
|
||||
DebridBadgePreviewDialog(
|
||||
BadgePreviewDialog(
|
||||
import = import,
|
||||
onDismiss = { previewImport = null },
|
||||
)
|
||||
|
|
@ -858,8 +853,8 @@ private fun DebridBadgeUrlManagerDialog(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun DebridBadgeUrlRow(
|
||||
import: DebridStreamBadgeImport,
|
||||
private fun BadgeUrlRow(
|
||||
import: StreamBadgeImport,
|
||||
showActiveChoice: Boolean,
|
||||
enabled: Boolean,
|
||||
onActivate: () -> Unit,
|
||||
|
|
@ -943,8 +938,8 @@ private fun DebridBadgeUrlRow(
|
|||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||
private fun DebridBadgePreviewDialog(
|
||||
import: DebridStreamBadgeImport,
|
||||
private fun BadgePreviewDialog(
|
||||
import: StreamBadgeImport,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val sections = remember(import) { badgePreviewSections(import) }
|
||||
|
|
@ -997,7 +992,14 @@ private fun DebridBadgePreviewDialog(
|
|||
verticalArrangement = Arrangement.spacedBy(5.dp),
|
||||
) {
|
||||
section.filters.forEach { filter ->
|
||||
DebridBadgePreviewChip(filter)
|
||||
ImportedBadgeChip(
|
||||
imageURL = filter.imageURL,
|
||||
name = filter.name,
|
||||
tagColor = filter.tagColor,
|
||||
tagStyle = filter.tagStyle,
|
||||
borderColor = filter.borderColor,
|
||||
size = ImportedBadgeChipSize.PREVIEW,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1016,54 +1018,24 @@ private fun DebridBadgePreviewDialog(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DebridBadgePreviewChip(filter: DebridStreamBadgeFilter) {
|
||||
val shape = RoundedCornerShape(6.dp)
|
||||
val backgroundColor = if (filter.tagStyle.equals("filled", ignoreCase = true)) {
|
||||
filter.tagColor.toBadgeColorOrNull()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val borderColor = filter.borderColor.toBadgeColorOrNull()
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(24.dp)
|
||||
.then(if (backgroundColor != null) Modifier.background(backgroundColor, shape) else Modifier)
|
||||
.then(if (borderColor != null) Modifier.border(1.dp, borderColor, shape) else Modifier)
|
||||
.padding(horizontal = 4.dp, vertical = 3.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
AsyncImage(
|
||||
model = filter.imageURL,
|
||||
contentDescription = filter.name,
|
||||
modifier = Modifier
|
||||
.height(18.dp)
|
||||
.widthIn(min = 38.dp, max = 112.dp)
|
||||
.clip(shape),
|
||||
contentScale = ContentScale.Fit,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private data class DebridBadgePreviewSection(
|
||||
private data class BadgePreviewSection(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val filters: List<DebridStreamBadgeFilter>,
|
||||
val filters: List<StreamBadgeFilter>,
|
||||
)
|
||||
|
||||
private fun badgePreviewSections(import: DebridStreamBadgeImport): List<DebridBadgePreviewSection> {
|
||||
private fun badgePreviewSections(import: StreamBadgeImport): List<BadgePreviewSection> {
|
||||
val filters = import.filters.filter { it.imageURL.isNotBlank() }
|
||||
if (filters.isEmpty()) return emptyList()
|
||||
|
||||
val filtersByGroupId = filters.groupBy { it.groupId }
|
||||
val usedGroupIds = mutableSetOf<String>()
|
||||
val sections = mutableListOf<DebridBadgePreviewSection>()
|
||||
val sections = mutableListOf<BadgePreviewSection>()
|
||||
import.groups.forEachIndexed { index, group ->
|
||||
val groupFilters = filtersByGroupId[group.id].orEmpty()
|
||||
if (groupFilters.isNotEmpty()) {
|
||||
usedGroupIds += group.id
|
||||
sections += DebridBadgePreviewSection(
|
||||
sections += BadgePreviewSection(
|
||||
id = group.id.ifBlank { "group-$index" },
|
||||
title = group.name.ifBlank { "Group ${index + 1}" },
|
||||
filters = groupFilters,
|
||||
|
|
@ -1073,7 +1045,7 @@ private fun badgePreviewSections(import: DebridStreamBadgeImport): List<DebridBa
|
|||
|
||||
val ungroupedFilters = filters.filter { it.groupId !in usedGroupIds }
|
||||
if (ungroupedFilters.isNotEmpty()) {
|
||||
sections += DebridBadgePreviewSection(
|
||||
sections += BadgePreviewSection(
|
||||
id = "other",
|
||||
title = "Other badges",
|
||||
filters = ungroupedFilters,
|
||||
|
|
@ -1082,16 +1054,6 @@ private fun badgePreviewSections(import: DebridStreamBadgeImport): List<DebridBa
|
|||
return sections
|
||||
}
|
||||
|
||||
private fun String.toBadgeColorOrNull(): Color? {
|
||||
val hex = trim().removePrefix("#")
|
||||
val argb = when (hex.length) {
|
||||
6 -> "FF$hex"
|
||||
8 -> hex
|
||||
else -> return null
|
||||
}
|
||||
return argb.toLongOrNull(16)?.let { Color(it) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DebridPreferenceRow(
|
||||
isTablet: Boolean,
|
||||
|
|
|
|||
|
|
@ -90,6 +90,9 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
|||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.core.ui.nuvioSafeBottomPadding
|
||||
import com.nuvio.app.features.debrid.BadgeChipDefaults
|
||||
import com.nuvio.app.features.debrid.ImportedBadgeChip
|
||||
import com.nuvio.app.features.debrid.ImportedBadgeChipSize
|
||||
import com.nuvio.app.features.debrid.DebridProviders
|
||||
import com.nuvio.app.features.debrid.DebridSettingsRepository
|
||||
import com.nuvio.app.features.player.PlayerSettingsRepository
|
||||
|
|
@ -1214,32 +1217,14 @@ private fun StreamItem.instantServiceLabel(): String? {
|
|||
|
||||
@Composable
|
||||
private fun StreamImportedBadge(badge: StreamBadge) {
|
||||
val shape = RoundedCornerShape(6.dp)
|
||||
val backgroundColor = if (badge.tagStyle.equals("filled", ignoreCase = true)) {
|
||||
badge.tagColor.toBadgeColorOrNull()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val borderColor = badge.borderColor.toBadgeColorOrNull()
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(20.dp)
|
||||
.then(if (backgroundColor != null) Modifier.background(backgroundColor, shape) else Modifier)
|
||||
.then(if (borderColor != null) Modifier.border(1.dp, borderColor, shape) else Modifier)
|
||||
.padding(horizontal = 3.dp, vertical = 2.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
AsyncImage(
|
||||
model = badge.imageURL,
|
||||
contentDescription = badge.name,
|
||||
modifier = Modifier
|
||||
.height(16.dp)
|
||||
.widthIn(min = 34.dp, max = 92.dp)
|
||||
.clip(shape),
|
||||
contentScale = ContentScale.Fit,
|
||||
)
|
||||
}
|
||||
ImportedBadgeChip(
|
||||
imageURL = badge.imageURL,
|
||||
name = badge.name,
|
||||
tagColor = badge.tagColor,
|
||||
tagStyle = badge.tagStyle,
|
||||
borderColor = badge.borderColor,
|
||||
size = ImportedBadgeChipSize.STREAM,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -1254,38 +1239,29 @@ private fun StreamFileSizeBadge(stream: StreamItem) {
|
|||
"${round(mib).toInt()} ${localizedByteUnit("MB")}"
|
||||
}
|
||||
|
||||
val badgeShape = BadgeChipDefaults.shape
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(20.dp)
|
||||
.clip(RoundedCornerShape(6.dp))
|
||||
.height(ImportedBadgeChipSize.STREAM.containerHeight)
|
||||
.clip(badgeShape)
|
||||
.background(Color(0xFF0A0C0C))
|
||||
.border(1.dp, Color(0xFF0A0C0C), RoundedCornerShape(6.dp))
|
||||
.padding(horizontal = 6.dp),
|
||||
.border(1.dp, Color(0xFF0A0C0C), badgeShape)
|
||||
.padding(horizontal = BadgeChipDefaults.fileSizeHorizontalPadding),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.streams_size, sizeLabel),
|
||||
style = MaterialTheme.typography.labelSmall.copy(
|
||||
fontSize = 10.sp,
|
||||
lineHeight = 12.sp,
|
||||
fontSize = BadgeChipDefaults.fileSizeFontSize,
|
||||
lineHeight = BadgeChipDefaults.fileSizeLineHeight,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
letterSpacing = 0.sp,
|
||||
letterSpacing = BadgeChipDefaults.fileSizeLetterSpacing,
|
||||
),
|
||||
color = Color.White,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.toBadgeColorOrNull(): Color? {
|
||||
val hex = trim().removePrefix("#")
|
||||
val argb = when (hex.length) {
|
||||
6 -> "FF$hex"
|
||||
8 -> hex
|
||||
else -> return null
|
||||
}
|
||||
return argb.toLongOrNull(16)?.let { Color(it) }
|
||||
}
|
||||
|
||||
private fun Long.toPlaybackClock(): String {
|
||||
val totalSeconds = (this / 1000L).coerceAtLeast(0L)
|
||||
val hours = totalSeconds / 3600L
|
||||
|
|
|
|||
|
|
@ -53,13 +53,13 @@ class DebridStreamPresentationTest {
|
|||
providerApiKeys = mapOf(DebridProviders.TORBOX_ID to "key"),
|
||||
streamNameTemplate = "{stream.rseMatched::join(' | ')}",
|
||||
streamDescriptionTemplate = "{stream.regexMatched::~REMUX[\"has-remux\"||\"missing-remux\"]}",
|
||||
streamBadgeRules = DebridStreamBadgeRules(
|
||||
streamBadgeRules = StreamBadgeRules(
|
||||
imports = listOf(
|
||||
DebridStreamBadgeImport(
|
||||
StreamBadgeImport(
|
||||
sourceUrl = "https://example.test/media-badges.json",
|
||||
isActive = false,
|
||||
filters = listOf(
|
||||
DebridStreamBadgeFilter(
|
||||
StreamBadgeFilter(
|
||||
name = "REMUX",
|
||||
pattern = "(?i)\\bremux\\b",
|
||||
imageURL = "https://example.test/remux.png",
|
||||
|
|
@ -68,18 +68,18 @@ class DebridStreamPresentationTest {
|
|||
textColor = "#FFFFFF",
|
||||
borderColor = "#27C04F",
|
||||
),
|
||||
DebridStreamBadgeFilter(
|
||||
StreamBadgeFilter(
|
||||
name = "Disabled",
|
||||
pattern = "(?i)\\bbluray\\b",
|
||||
isEnabled = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
DebridStreamBadgeImport(
|
||||
StreamBadgeImport(
|
||||
sourceUrl = "https://example.test/audio-badges.json",
|
||||
isActive = true,
|
||||
filters = listOf(
|
||||
DebridStreamBadgeFilter(
|
||||
StreamBadgeFilter(
|
||||
name = "TRUEHD",
|
||||
pattern = "(?i)\\btruehd\\b",
|
||||
imageURL = "https://example.test/truehd.png",
|
||||
|
|
@ -103,7 +103,7 @@ class DebridStreamPresentationTest {
|
|||
|
||||
@Test
|
||||
fun `parses fusion badge url payload shape`() {
|
||||
val importedRules = DebridStreamBadgeRulesParser.parse(
|
||||
val importedRules = StreamBadgeRulesParser.parse(
|
||||
sourceUrl = "https://example.test/fusion-tags-ume.json",
|
||||
payload = """
|
||||
{
|
||||
|
|
@ -160,12 +160,12 @@ class DebridStreamPresentationTest {
|
|||
settings = DebridSettings(
|
||||
enabled = true,
|
||||
providerApiKeys = mapOf(DebridProviders.TORBOX_ID to "key"),
|
||||
streamBadgeRules = DebridStreamBadgeRules(
|
||||
streamBadgeRules = StreamBadgeRules(
|
||||
imports = listOf(
|
||||
DebridStreamBadgeImport(
|
||||
StreamBadgeImport(
|
||||
sourceUrl = "https://example.test/badges.json",
|
||||
filters = listOf(
|
||||
DebridStreamBadgeFilter(
|
||||
StreamBadgeFilter(
|
||||
name = "REMUX 1",
|
||||
pattern = "(?i)\\bremux\\b",
|
||||
imageURL = "https://example.test/remux-t1.png",
|
||||
|
|
|
|||
Loading…
Reference in a new issue