Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions app/src/main/kotlin/com/arflix/tv/data/model/IptvModels.kt
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,19 @@ data class IptvSnapshot(
val epgWarning: String? = null,
val loadedAt: Instant = Instant.now()
)

/**
* Lightweight helper to handle playlistId|groupName composite keys without
* unnecessary string allocations in UI loops.
*/
@JvmInline
value class PlaylistGroupKey(val key: String) {
val playlistId: String get() = key.substringBefore('|')
val groupName: String get() = key.substringAfter('|', missingDelimiterValue = key)

companion object {
fun build(playlistId: String, groupName: String): String {
return "$playlistId|$groupName"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,6 @@ class CloudSyncRepository @Inject constructor(
val dnsProvider: String = "system",
val subtitleUsageJson: String = "",
val subtitleSettingsUpdatedAt: Long = 0L,
val iptvHiddenGroups: String = "",
val iptvGroupOrder: String = "",
val secondarySubtitle: String = "Off",
val filterSubtitlesByLanguage: Boolean = true,
val homeServerConnectionJson: String? = null,
Expand Down Expand Up @@ -362,8 +360,6 @@ class CloudSyncRepository @Inject constructor(
subtitleOffset = prefs[subtitleOffsetKeyFor(profile.id)] ?: "Bottom",
subtitleStyle = prefs[subtitleStyleKeyFor(profile.id)] ?: "Bold",
subtitleStylized = prefs[subtitleStylizedKeyFor(profile.id)] ?: true,
iptvHiddenGroups = prefs[iptvHiddenGroupsKeyFor(profile.id)] ?: "",
iptvGroupOrder = prefs[iptvGroupOrderKeyFor(profile.id)] ?: "",
secondarySubtitle = prefs[secondarySubtitleKeyFor(profile.id)] ?: "Off",
filterSubtitlesByLanguage = prefs[filterSubtitlesByLanguageKeyFor(profile.id)] ?: true,
homeServerConnectionJson = homeServerRepository.exportCloudConnectionsJsonForProfile(profile.id),
Expand Down Expand Up @@ -811,8 +807,6 @@ class CloudSyncRepository @Inject constructor(
prefs[subtitleOffsetKeyFor(profileId)] = state.subtitleOffset
prefs[subtitleStyleKeyFor(profileId)] = state.subtitleStyle
prefs[subtitleStylizedKeyFor(profileId)] = state.subtitleStylized
if (state.iptvHiddenGroups.isNotBlank()) prefs[iptvHiddenGroupsKeyFor(profileId)] = state.iptvHiddenGroups
if (state.iptvGroupOrder.isNotBlank()) prefs[iptvGroupOrderKeyFor(profileId)] = state.iptvGroupOrder
prefs[secondarySubtitleKeyFor(profileId)] = state.secondarySubtitle.ifBlank { "Off" }
prefs[filterSubtitlesByLanguageKeyFor(profileId)] = state.filterSubtitlesByLanguage
state.homeServerConnectionJson?.let { homeServerConnectionJson ->
Expand Down
73 changes: 55 additions & 18 deletions app/src/main/kotlin/com/arflix/tv/data/repository/IptvRepository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.Deferred
import com.arflix.tv.data.model.PlaylistGroupKey
import kotlinx.coroutines.launch
import com.arflix.tv.network.OkHttpProvider
import okhttp3.OkHttpClient
Expand Down Expand Up @@ -830,9 +831,9 @@ class IptvRepository @Inject constructor(
invalidationBus.markDirty(CloudSyncScope.IPTV, profileManager.getProfileIdSync(), "toggle favorite group")
}

suspend fun toggleHiddenGroup(groupName: String) {
val trimmed = groupName.trim()
if (trimmed.isEmpty()) return
suspend fun toggleHiddenGroup(playlistId: String, groupName: String) {
val trimmed = PlaylistGroupKey.build(playlistId, groupName.trim())
if (groupName.trim().isEmpty()) return
context.settingsDataStore.edit { prefs ->
val existing = decodeHiddenGroups(prefs).toMutableList()
if (existing.contains(trimmed)) {
Expand All @@ -845,11 +846,12 @@ class IptvRepository @Inject constructor(
invalidationBus.markDirty(CloudSyncScope.IPTV, profileManager.getProfileIdSync(), "toggle hidden group")
}

suspend fun moveGroupUp(groupName: String, currentGroups: List<String> = emptyList()) {
val target = groupName.trim()
if (target.isEmpty()) return
suspend fun moveGroupUp(playlistId: String, groupName: String, currentGroups: List<String> = emptyList()) {
val target = PlaylistGroupKey.build(playlistId, groupName.trim())
if (groupName.trim().isEmpty()) return
val currentKeys = currentGroups.map { PlaylistGroupKey.build(playlistId, it.trim()) }
context.settingsDataStore.edit { prefs ->
val order = mergedGroupOrder(decodeGroupOrder(prefs), currentGroups)
val order = mergedGroupOrder(decodeGroupOrder(prefs), currentKeys)
if (order.isEmpty()) return@edit
val idx = order.indexOf(target)
if (idx > 0) { order.removeAt(idx); order.add(idx - 1, target) }
Expand All @@ -858,25 +860,27 @@ class IptvRepository @Inject constructor(
invalidationBus.markDirty(CloudSyncScope.IPTV, profileManager.getProfileIdSync(), "move group up")
}

suspend fun moveGroupToTop(groupName: String, currentGroups: List<String> = emptyList()) {
val target = groupName.trim()
if (target.isEmpty()) return
suspend fun moveGroupToTop(playlistId: String, groupName: String, currentGroups: List<String> = emptyList()) {
val target = PlaylistGroupKey.build(playlistId, groupName.trim())
if (groupName.trim().isEmpty()) return
val currentKeys = currentGroups.map { PlaylistGroupKey.build(playlistId, it.trim()) }
context.settingsDataStore.edit { prefs ->
val order = mergedGroupOrder(decodeGroupOrder(prefs), currentGroups)
val order = mergedGroupOrder(decodeGroupOrder(prefs), currentKeys)
if (order.isEmpty()) return@edit
if (target !in order && currentGroups.map { it.trim() }.contains(target)) order.add(target)
if (target !in order && currentKeys.contains(target)) order.add(target)
order.remove(target)
order.add(0, target)
prefs[groupOrderKey()] = gson.toJson(order)
}
invalidationBus.markDirty(CloudSyncScope.IPTV, profileManager.getProfileIdSync(), "move group to top")
}

suspend fun moveGroupDown(groupName: String, currentGroups: List<String> = emptyList()) {
val target = groupName.trim()
if (target.isEmpty()) return
suspend fun moveGroupDown(playlistId: String, groupName: String, currentGroups: List<String> = emptyList()) {
val target = PlaylistGroupKey.build(playlistId, groupName.trim())
if (groupName.trim().isEmpty()) return
val currentKeys = currentGroups.map { PlaylistGroupKey.build(playlistId, it.trim()) }
context.settingsDataStore.edit { prefs ->
val order = mergedGroupOrder(decodeGroupOrder(prefs), currentGroups)
val order = mergedGroupOrder(decodeGroupOrder(prefs), currentKeys)
if (order.isEmpty()) return@edit
val idx = order.indexOf(target)
if (idx >= 0 && idx < order.size - 1) { order.removeAt(idx); order.add(idx + 1, target) }
Expand All @@ -885,6 +889,15 @@ class IptvRepository @Inject constructor(
invalidationBus.markDirty(CloudSyncScope.IPTV, profileManager.getProfileIdSync(), "move group down")
}

suspend fun resetGroupOrder(playlistId: String) {
context.settingsDataStore.edit { prefs ->
val existing = decodeGroupOrder(prefs).toMutableList()
existing.removeAll { PlaylistGroupKey(it).playlistId == playlistId }
prefs[groupOrderKey()] = gson.toJson(existing)
}
invalidationBus.markDirty(CloudSyncScope.IPTV, profileManager.getProfileIdSync(), "reset group order")
}

suspend fun toggleFavoriteChannel(channelId: String) {
val trimmed = channelId.trim()
if (trimmed.isEmpty()) return
Expand Down Expand Up @@ -1746,7 +1759,19 @@ class IptvRepository @Inject constructor(
if (raw.isBlank()) return emptyList()
return runCatching {
val type = TypeToken.getParameterized(List::class.java, String::class.java).type
gson.fromJson<List<String>>(raw, type)?.map { it.trim() }?.filter { it.isNotBlank() }?.distinct() ?: emptyList()
val list = gson.fromJson<List<String>>(raw, type)?.map { it.trim() }?.filter { it.isNotBlank() }?.distinct() ?: emptyList()
if (list.any { !it.contains('|') }) {
val playlistsRaw = prefs[playlistsKey()].orEmpty()
if (playlistsRaw.isBlank()) {
list
} else {
val playlists = decodePlaylists(playlistsRaw)
val firstId = playlists.firstOrNull()?.id
if (firstId != null) {
list.map { if (it.contains('|')) it else "$firstId|$it" }.distinct()
} else list
}
} else list
}.getOrDefault(emptyList())
}

Expand All @@ -1755,7 +1780,19 @@ class IptvRepository @Inject constructor(
if (raw.isBlank()) return emptyList()
return runCatching {
val type = TypeToken.getParameterized(List::class.java, String::class.java).type
gson.fromJson<List<String>>(raw, type)?.map { it.trim() }?.filter { it.isNotBlank() }?.distinct() ?: emptyList()
val list = gson.fromJson<List<String>>(raw, type)?.map { it.trim() }?.filter { it.isNotBlank() }?.distinct() ?: emptyList()
if (list.any { !it.contains('|') }) {
val playlistsRaw = prefs[playlistsKey()].orEmpty()
if (playlistsRaw.isBlank()) {
list
} else {
val playlists = decodePlaylists(playlistsRaw)
val firstId = playlists.firstOrNull()?.id
if (firstId != null) {
list.map { if (it.contains('|')) it else "$firstId|$it" }.distinct()
} else list
}
} else list
}.getOrDefault(emptyList())
}

Expand Down
Loading
Loading