feat: add support for custom addon names

This commit is contained in:
tapframe 2026-04-07 16:28:20 +05:30
parent 6c17efdbef
commit ebcccb9b4c
10 changed files with 51 additions and 25 deletions

View file

@ -44,6 +44,7 @@ data class AddonBehaviorHints(
data class ManagedAddon( data class ManagedAddon(
val manifestUrl: String, val manifestUrl: String,
val manifest: AddonManifest? = null, val manifest: AddonManifest? = null,
val userSetName: String? = null,
val isRefreshing: Boolean = false, val isRefreshing: Boolean = false,
val errorMessage: String? = null, val errorMessage: String? = null,
) { ) {
@ -51,7 +52,9 @@ data class ManagedAddon(
get() = manifest != null get() = manifest != null
val displayTitle: String val displayTitle: String
get() = manifest?.name ?: manifestUrl.substringBefore("?").substringAfterLast("/").ifBlank { "Addon" } get() = userSetName?.takeIf { it.isNotBlank() && it != manifest?.name }
?: manifest?.name
?: manifestUrl.substringBefore("?").substringAfterLast("/").ifBlank { "Addon" }
} }
data class AddonsUiState( data class AddonsUiState(

View file

@ -108,6 +108,13 @@ object AddonRepository {
} }
.decodeList<AddonRow>() .decodeList<AddonRow>()
val namesByUrl = mutableMapOf<String, String>()
rows.forEach { row ->
if (!row.name.isNullOrBlank()) {
namesByUrl[ensureManifestSuffix(row.url)] = row.name
}
}
val urls = dedupeManifestUrls(rows.map { it.url }) val urls = dedupeManifestUrls(rows.map { it.url })
log.i { "pullFromServer() — server returned ${rows.size} addons" } log.i { "pullFromServer() — server returned ${rows.size} addons" }
urls.forEachIndexed { i, u -> log.d { " server[$i]: $u" } } urls.forEachIndexed { i, u -> log.d { " server[$i]: $u" } }
@ -164,7 +171,7 @@ object AddonRepository {
val existingByUrl = _uiState.value.addons.associateBy(ManagedAddon::manifestUrl) val existingByUrl = _uiState.value.addons.associateBy(ManagedAddon::manifestUrl)
_uiState.value = AddonsUiState( _uiState.value = AddonsUiState(
addons = urls.map { url -> addons = urls.map { url ->
existingByUrl[url].toPendingAddon(url) existingByUrl[url].toPendingAddon(url, namesByUrl[url])
}, },
) )
persist() persist()
@ -311,7 +318,7 @@ object AddonRepository {
.mapIndexed { index, addon -> .mapIndexed { index, addon ->
AddonPushItem( AddonPushItem(
url = addon.manifestUrl, url = addon.manifestUrl,
name = addon.manifest?.name ?: "", name = addon.userSetName?.takeIf { it.isNotBlank() } ?: addon.manifest?.name ?: "",
enabled = true, enabled = true,
sortOrder = index, sortOrder = index,
) )
@ -369,21 +376,27 @@ object AddonRepository {
} }
} }
private fun ManagedAddon?.toPendingAddon(manifestUrl: String): ManagedAddon = private fun ManagedAddon?.toPendingAddon(manifestUrl: String, userSetName: String? = null): ManagedAddon =
when { when {
this == null -> ManagedAddon( this == null -> ManagedAddon(
manifestUrl = manifestUrl, manifestUrl = manifestUrl,
isRefreshing = true, isRefreshing = true,
userSetName = userSetName,
) )
manifest != null -> copy( manifest != null -> copy(
manifestUrl = manifestUrl, manifestUrl = manifestUrl,
isRefreshing = false, isRefreshing = false,
userSetName = userSetName ?: this.userSetName,
)
isRefreshing -> copy(
manifestUrl = manifestUrl,
userSetName = userSetName ?: this.userSetName,
) )
isRefreshing -> copy(manifestUrl = manifestUrl)
else -> copy( else -> copy(
manifestUrl = manifestUrl, manifestUrl = manifestUrl,
isRefreshing = true, isRefreshing = true,
errorMessage = null, errorMessage = null,
userSetName = userSetName ?: this.userSetName,
) )
} }

View file

@ -330,7 +330,7 @@ private fun InstalledAddonCard(
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text( Text(
text = manifest?.name ?: addon.displayTitle, text = addon.displayTitle,
style = MaterialTheme.typography.headlineLarge, style = MaterialTheme.typography.headlineLarge,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
maxLines = 2, maxLines = 2,

View file

@ -162,7 +162,7 @@ object CollectionRepository {
.map { catalog -> .map { catalog ->
AvailableCatalog( AvailableCatalog(
addonId = manifest.id, addonId = manifest.id,
addonName = manifest.name, addonName = addon.displayTitle,
type = catalog.type, type = catalog.type,
catalogId = catalog.id, catalogId = catalog.id,
catalogName = catalog.name, catalogName = catalog.name,

View file

@ -24,7 +24,7 @@ fun buildHomeCatalogDefinitions(addons: List<ManagedAddon>): List<HomeCatalogDef
HomeCatalogDefinition( HomeCatalogDefinition(
key = "${manifest.id}:${catalog.type}:${catalog.id}", key = "${manifest.id}:${catalog.type}:${catalog.id}",
defaultTitle = "${catalog.name} - ${catalog.type.displayLabel()}", defaultTitle = "${catalog.name} - ${catalog.type.displayLabel()}",
addonName = manifest.name, addonName = addon.displayTitle,
manifestUrl = addon.manifestUrl, manifestUrl = addon.manifestUrl,
type = catalog.type, type = catalog.type,
catalogId = catalog.id, catalogId = catalog.id,

View file

@ -629,7 +629,7 @@ fun PlayerScreen(
) )
val installedAddonNames = AddonRepository.uiState.value.addons val installedAddonNames = AddonRepository.uiState.value.addons
.mapNotNull { it.manifest?.name } .map { it.displayTitle }
.toSet() .toSet()
val timeoutMs = settings.streamAutoPlayTimeoutSeconds * 1000L val timeoutMs = settings.streamAutoPlayTimeoutSeconds * 1000L

View file

@ -165,6 +165,10 @@ object PlayerStreamsRepository {
return return
} }
val addonDisplayNames = installedAddons.associate {
(it.manifest?.id ?: it.manifestUrl) to it.displayTitle
}
val streamAddons = installedAddons val streamAddons = installedAddons
.mapNotNull { it.manifest } .mapNotNull { it.manifest }
.filter { manifest -> .filter { manifest ->
@ -186,7 +190,7 @@ object PlayerStreamsRepository {
val initialGroups = streamAddons.map { manifest -> val initialGroups = streamAddons.map { manifest ->
AddonStreamGroup( AddonStreamGroup(
addonName = manifest.name, addonName = addonDisplayNames[manifest.id] ?: manifest.name,
addonId = manifest.id, addonId = manifest.id,
streams = emptyList(), streams = emptyList(),
isLoading = true, isLoading = true,
@ -214,16 +218,17 @@ object PlayerStreamsRepository {
.removeSuffix("/manifest.json") .removeSuffix("/manifest.json")
val url = "$baseUrl/stream/$type/$encodedId.json" val url = "$baseUrl/stream/$type/$encodedId.json"
val displayName = addonDisplayNames[manifest.id] ?: manifest.name
runCatching { runCatching {
val payload = httpGetText(url) val payload = httpGetText(url)
StreamParser.parse(payload, manifest.name, manifest.id) StreamParser.parse(payload, displayName, manifest.id)
}.fold( }.fold(
onSuccess = { streams -> onSuccess = { streams ->
AddonStreamGroup(manifest.name, manifest.id, streams, isLoading = false) AddonStreamGroup(displayName, manifest.id, streams, isLoading = false)
}, },
onFailure = { err -> onFailure = { err ->
log.w(err) { "Failed: ${manifest.name}" } log.w(err) { "Failed: ${displayName}" }
AddonStreamGroup(manifest.name, manifest.id, emptyList(), isLoading = false, error = err.message) AddonStreamGroup(displayName, manifest.id, emptyList(), isLoading = false, error = err.message)
}, },
) )
} }

View file

@ -71,7 +71,7 @@ object SubtitleRepository {
id = id, id = id,
url = url, url = url,
language = lang, language = lang,
display = "${formatLanguage(lang)} (${manifest.name})", display = "${formatLanguage(lang)} (${addon.displayTitle})",
) )
) )
} }

View file

@ -288,7 +288,7 @@ object SearchRepository {
val genreExtra = catalog.genreExtra() val genreExtra = catalog.genreExtra()
DiscoverCatalogOption( DiscoverCatalogOption(
key = "${manifest.id}:${catalog.type}:${catalog.id}", key = "${manifest.id}:${catalog.type}:${catalog.id}",
addonName = manifest.name, addonName = addon.displayTitle,
manifestUrl = addon.manifestUrl, manifestUrl = addon.manifestUrl,
type = catalog.type, type = catalog.type,
catalogId = catalog.id, catalogId = catalog.id,
@ -314,8 +314,8 @@ object SearchRepository {
return HomeCatalogSection( return HomeCatalogSection(
key = "${manifest.id}:search:$type:$catalogId:${query.lowercase()}", key = "${manifest.id}:search:$type:$catalogId:${query.lowercase()}",
title = "$catalogName - ${type.displayLabel()}", title = "$catalogName - ${type.displayLabel()}",
subtitle = manifest.name, subtitle = addon.displayTitle,
addonName = manifest.name, addonName = addon.displayTitle,
type = type, type = type,
manifestUrl = manifest.transportUrl, manifestUrl = manifest.transportUrl,
catalogId = catalogId, catalogId = catalogId,

View file

@ -127,6 +127,10 @@ object StreamsRepository {
return return
} }
val addonDisplayNames = installedAddons.associate {
(it.manifest?.id ?: it.manifestUrl) to it.displayTitle
}
val streamAddons = installedAddons val streamAddons = installedAddons
.mapNotNull { it.manifest } .mapNotNull { it.manifest }
.filter { manifest -> .filter { manifest ->
@ -151,7 +155,7 @@ object StreamsRepository {
// Initialise loading placeholders // Initialise loading placeholders
val initialGroups = streamAddons.map { manifest -> val initialGroups = streamAddons.map { manifest ->
AddonStreamGroup( AddonStreamGroup(
addonName = manifest.name, addonName = addonDisplayNames[manifest.id] ?: manifest.name,
addonId = manifest.id, addonId = manifest.id,
streams = emptyList(), streams = emptyList(),
isLoading = true, isLoading = true,
@ -182,7 +186,7 @@ object StreamsRepository {
val totalTasks = streamAddons.size + pluginRemainingByAddonId.values.sum() val totalTasks = streamAddons.size + pluginRemainingByAddonId.values.sum()
val installedAddonNames = installedAddons val installedAddonNames = installedAddons
.mapNotNull { it.manifest?.name } .map { it.displayTitle }
.toSet() .toSet()
var autoSelectTriggered = false var autoSelectTriggered = false
var timeoutElapsed = false var timeoutElapsed = false
@ -237,27 +241,28 @@ object StreamsRepository {
val url = "$baseUrl/stream/$type/$encodedId.json" val url = "$baseUrl/stream/$type/$encodedId.json"
log.d { "Fetching streams from: $url" } log.d { "Fetching streams from: $url" }
val displayName = addonDisplayNames[manifest.id] ?: manifest.name
val group = runCatching { val group = runCatching {
val payload = httpGetText(url) val payload = httpGetText(url)
StreamParser.parse( StreamParser.parse(
payload = payload, payload = payload,
addonName = manifest.name, addonName = displayName,
addonId = manifest.id, addonId = manifest.id,
) )
}.fold( }.fold(
onSuccess = { streams -> onSuccess = { streams ->
log.d { "Got ${streams.size} streams from ${manifest.name}" } log.d { "Got ${streams.size} streams from ${displayName}" }
AddonStreamGroup( AddonStreamGroup(
addonName = manifest.name, addonName = displayName,
addonId = manifest.id, addonId = manifest.id,
streams = streams, streams = streams,
isLoading = false, isLoading = false,
) )
}, },
onFailure = { err -> onFailure = { err ->
log.w(err) { "Failed to fetch streams from ${manifest.name}" } log.w(err) { "Failed to fetch streams from ${displayName}" }
AddonStreamGroup( AddonStreamGroup(
addonName = manifest.name, addonName = displayName,
addonId = manifest.id, addonId = manifest.id,
streams = emptyList(), streams = emptyList(),
isLoading = false, isLoading = false,