From de4635f2236356c5317856fa25d11d3eadb338e0 Mon Sep 17 00:00:00 2001 From: Alexey Rochev Date: Sat, 8 Aug 2026 22:07:03 +0300 Subject: [PATCH 1/7] Fix error messages when failing to start/stop/verify torrents from torrents list --- .../torrentslistfragment/TorrentsListFragmentViewModel.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/TorrentsListFragmentViewModel.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/TorrentsListFragmentViewModel.kt index b7fbb929..5e82a8c2 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/TorrentsListFragmentViewModel.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/TorrentsListFragmentViewModel.kt @@ -356,16 +356,16 @@ class TorrentsListFragmentViewModel(application: Application, savedStateHandle: val torrentOperations: TorrentsOperations = object : TorrentsOperations { override fun start(ids: Set) = - performRequestAndRefresh(R.string.torrents_reannounce_error) { startTorrents(ids) } + performRequestAndRefresh(R.string.torrents_start_error) { startTorrents(ids) } override fun startNow(ids: Set) = - performRequestAndRefresh(R.string.torrents_reannounce_error) { startTorrentsNow(ids) } + performRequestAndRefresh(R.string.torrents_start_error) { startTorrentsNow(ids) } override fun stop(ids: Set) = - performRequestAndRefresh(R.string.torrents_reannounce_error) { stopTorrents(ids) } + performRequestAndRefresh(R.string.torrents_pause_error) { stopTorrents(ids) } override fun verify(ids: Set) = - performRequestAndRefresh(R.string.torrents_reannounce_error) { verifyTorrents(ids) } + performRequestAndRefresh(R.string.torrents_check_error) { verifyTorrents(ids) } override fun reannounce(ids: Set) = performRequestAndRefresh(R.string.torrents_reannounce_error) { reannounceTorrents(ids) } From d9169dba92b71201a058f9fe5636ad459dd0e614 Mon Sep 17 00:00:00 2001 From: Alexey Rochev Date: Sat, 8 Aug 2026 22:50:26 +0300 Subject: [PATCH 2/7] Don't call checkIfTorrentExists twice --- .../org/equeim/tremotesf/ui/addtorrent/AddTorrentFileModel.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/addtorrent/AddTorrentFileModel.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/addtorrent/AddTorrentFileModel.kt index c0caccb4..714c4b48 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/addtorrent/AddTorrentFileModel.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/addtorrent/AddTorrentFileModel.kt @@ -166,7 +166,6 @@ class AddTorrentFileModelImpl( infoHashV1 = parseResult.infoHashV1 trackers = parseResult.trackers - checkIfTorrentExists() if (checkIfTorrentExists()) { return@withContext LoadingState.Aborted } From 422a825909707873c08ce1b4096c057e703b9153 Mon Sep 17 00:00:00 2001 From: Alexey Rochev Date: Sat, 8 Aug 2026 23:06:48 +0300 Subject: [PATCH 3/7] Fix showing notification permission prompt --- .../tremotesf/ui/torrentslistfragment/TorrentsListFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/TorrentsListFragment.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/TorrentsListFragment.kt index 73ce4423..e56380ab 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/TorrentsListFragment.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/TorrentsListFragment.kt @@ -412,7 +412,7 @@ private fun ShowNotificationPermissionSnackbar( var showSnackbar: Boolean by rememberSaveable { mutableStateOf(false) } LaunchedEffect(notificationPermissionHelperState, checkNotificationPermission) { if (checkNotificationPermission.filterNotNull() - .first() && notificationPermissionHelperState.permissionGranted + .first() && !notificationPermissionHelperState.permissionGranted ) { onCheckedNotificationPermission() showSnackbar = true From 5be3e94dc68e588b7829b897bdb71e6fc45a88c4 Mon Sep 17 00:00:00 2001 From: Alexey Rochev Date: Mon, 10 Aug 2026 02:24:21 +0300 Subject: [PATCH 4/7] Rework how label/tracker/directory filters are handled in FiltersBottomSheet --- .../FiltersBottomSheet.kt | 105 +++++++++++------- 1 file changed, 65 insertions(+), 40 deletions(-) diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/FiltersBottomSheet.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/FiltersBottomSheet.kt index 44407a2b..6c12eaee 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/FiltersBottomSheet.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/FiltersBottomSheet.kt @@ -38,6 +38,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.flow.MutableStateFlow import org.equeim.tremotesf.R import org.equeim.tremotesf.common.AlphanumericComparator +import org.equeim.tremotesf.rpc.requests.NormalizedRpcPath import org.equeim.tremotesf.rpc.requests.Torrent import org.equeim.tremotesf.rpc.toNativeSeparators import org.equeim.tremotesf.ui.ComponentPreview @@ -176,16 +177,22 @@ private fun FiltersBottomSheetContent( ) if (labelsEnabled.value) { + val currentLabelFilterString by sortAndFilterSettings.labelFilter.collectAsStateWithLifecycle() + val currentLabelFilter = remember { + derivedStateOf { + calculatedFilters.labels.find { it.label == currentLabelFilterString } + ?: CalculatedFilters.LabelFilter(currentLabelFilterString, 0) + } + } TremotesfComboBox( - currentItem = sortAndFilterSettings.labelFilter.collectAsStateWithLifecycle()::value, - updateCurrentItem = sortAndFilterSettings::setLabelFilter, - items = calculatedFilters.sortedLabels, + currentItem = currentLabelFilter::value, + updateCurrentItem = { sortAndFilterSettings.setLabelFilter(it.label) }, + items = calculatedFilters.labels, itemDisplayString = { - val count = calculatedFilters.labelsCounts.getOrDefault(it, 0) - if (it.isEmpty()) { - stringResource(R.string.torrents_all, count) + if (it.label == "") { + stringResource(R.string.torrents_all, it.torrentsCount) } else { - stringResource(R.string.directories_spinner_text, it, count) + stringResource(R.string.directories_spinner_text, it.label, it.torrentsCount) } }, label = R.string.labels, @@ -193,32 +200,47 @@ private fun FiltersBottomSheetContent( ) } + val currentTrackerFilterString by sortAndFilterSettings.trackerFilter.collectAsStateWithLifecycle() + val currentTrackerFilter = remember { + derivedStateOf { + calculatedFilters.trackers.find { it.trackerSite == currentTrackerFilterString } + ?: CalculatedFilters.TrackerFilter(currentTrackerFilterString, 0) + } + } TremotesfComboBox( - currentItem = sortAndFilterSettings.trackerFilter.collectAsStateWithLifecycle()::value, - updateCurrentItem = sortAndFilterSettings::setTrackerFilter, - items = calculatedFilters.sortedTrackers, + currentItem = currentTrackerFilter::value, + updateCurrentItem = { sortAndFilterSettings.setTrackerFilter(it.trackerSite) }, + items = calculatedFilters.trackers, itemDisplayString = { - val count = calculatedFilters.trackersCounts.getOrDefault(it, 0) - if (it.isEmpty()) { - stringResource(R.string.torrents_all, count) + if (it.trackerSite.isEmpty()) { + stringResource(R.string.torrents_all, it.torrentsCount) } else { - stringResource(R.string.trackers_spinner_text, it, count) + stringResource(R.string.trackers_spinner_text, it.trackerSite, it.torrentsCount) } }, label = R.string.trackers, modifier = Modifier.fillMaxWidth() ) + val currentDirectoryFilterString by sortAndFilterSettings.directoryFilter.collectAsStateWithLifecycle() + val currentDirectoryFilter = remember { + derivedStateOf { + calculatedFilters.directories.find { it.directory == currentDirectoryFilterString } + ?: CalculatedFilters.DirectoryFilter( + directory = currentDirectoryFilterString, + torrentsCount = 0 + ) + } + } TremotesfComboBox( - currentItem = sortAndFilterSettings.directoryFilter.collectAsStateWithLifecycle()::value, - updateCurrentItem = sortAndFilterSettings::setDirectoryFilter, - items = calculatedFilters.sortedDirectories, + currentItem = currentDirectoryFilter::value, + updateCurrentItem = { sortAndFilterSettings.setDirectoryFilter(it.directory) }, + items = calculatedFilters.directories, itemDisplayString = { - val count = calculatedFilters.directoriesCounts.getOrDefault(it, 0) - if (it.isEmpty()) { - stringResource(R.string.torrents_all, count) + if (it.directory.isEmpty()) { + stringResource(R.string.torrents_all, it.torrentsCount) } else { - stringResource(R.string.directories_spinner_text, it, count) + stringResource(R.string.directories_spinner_text, it.directory, it.torrentsCount) } }, label = R.string.directories, @@ -265,13 +287,14 @@ private fun SortOrderButtons( private data class CalculatedFilters( val statusFilterModesCounts: Map, - val sortedLabels: List, - val labelsCounts: Map, - val sortedTrackers: List, - val trackersCounts: Map, - val sortedDirectories: List, - val directoriesCounts: Map -) + val labels: List, + val trackers: List, + val directories: List +) { + data class LabelFilter(val label: String, val torrentsCount: Int) + data class TrackerFilter(val trackerSite: String, val torrentsCount: Int) + data class DirectoryFilter(val directory: String, val torrentsCount: Int) +} private fun calculateFilters( torrents: List, @@ -280,12 +303,12 @@ private fun calculateFilters( ): CalculatedFilters { val modes = mutableMapOf(StatusFilterMode.All to torrents.size) val labels = if (labelsEnabled) { - mutableMapOf("" to torrents.size) + sortedMapOf(comparator, "" to torrents.size) } else { null } - val trackers = mutableMapOf("" to torrents.size) - val directories = mutableMapOf("" to torrents.size) + val trackers = sortedMapOf(comparator, "" to torrents.size) + val directories = sortedMapOf(nullsFirst(compareBy(comparator, NormalizedRpcPath::value)), null to torrents.size) for (torrent in torrents) { for (mode in STATUS_FILTER_MODES_WITHOUT_ALL) { if (statusFilterAcceptsTorrent(torrent, mode)) { @@ -300,21 +323,23 @@ private fun calculateFilters( for (tracker in torrent.trackerSites) { trackers.compute(tracker, IncrementCount) } - directories.compute(torrent.downloadDirectory.toNativeSeparators(), IncrementCount) + directories.compute(torrent.downloadDirectory, IncrementCount) } return CalculatedFilters( statusFilterModesCounts = modes, - sortedLabels = labels?.keys?.sortedWith(comparator) ?: emptyList(), - labelsCounts = labels ?: emptyMap(), - sortedTrackers = trackers.keys.sortedWith(comparator), - trackersCounts = trackers, - sortedDirectories = directories.keys.sortedWith(comparator), - directoriesCounts = directories + labels = labels?.map { CalculatedFilters.LabelFilter(it.key, it.value) }.orEmpty(), + trackers = trackers.map { CalculatedFilters.TrackerFilter(it.key, it.value) }, + directories = directories.map { + CalculatedFilters.DirectoryFilter( + directory = it.key?.toNativeSeparators().orEmpty(), + torrentsCount = it.value + ) + } ) } -private object IncrementCount : BiFunction { - override fun apply(key: Any, count: Int?): Int { +private object IncrementCount : BiFunction { + override fun apply(key: Any?, count: Int?): Int { return (count ?: 0) + 1 } } From 426e76add664c937f7d7a07c63428fdcdb73ef46 Mon Sep 17 00:00:00 2001 From: Alexey Rochev Date: Mon, 10 Aug 2026 02:26:30 +0300 Subject: [PATCH 5/7] Save normalized directory as filter --- .../FiltersBottomSheet.kt | 23 +++++++++++-------- .../TorrentsListFragmentViewModel.kt | 20 ++++++++++------ .../kotlin/org/equeim/tremotesf/rpc/Paths.kt | 8 ++----- .../equeim/tremotesf/rpc/requests/Types.kt | 6 +++++ 4 files changed, 34 insertions(+), 23 deletions(-) diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/FiltersBottomSheet.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/FiltersBottomSheet.kt index 6c12eaee..eee12365 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/FiltersBottomSheet.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/FiltersBottomSheet.kt @@ -38,6 +38,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.flow.MutableStateFlow import org.equeim.tremotesf.R import org.equeim.tremotesf.common.AlphanumericComparator +import org.equeim.tremotesf.rpc.normalizePath import org.equeim.tremotesf.rpc.requests.NormalizedRpcPath import org.equeim.tremotesf.rpc.requests.Torrent import org.equeim.tremotesf.rpc.toNativeSeparators @@ -222,25 +223,26 @@ private fun FiltersBottomSheetContent( modifier = Modifier.fillMaxWidth() ) - val currentDirectoryFilterString by sortAndFilterSettings.directoryFilter.collectAsStateWithLifecycle() - val currentDirectoryFilter = remember { + val currentDirectoryFilter by sortAndFilterSettings.directoryFilter.collectAsStateWithLifecycle() + val currentDirectoryFilterCalculated = remember { derivedStateOf { - calculatedFilters.directories.find { it.directory == currentDirectoryFilterString } + calculatedFilters.directories.find { it.directory == currentDirectoryFilter } ?: CalculatedFilters.DirectoryFilter( - directory = currentDirectoryFilterString, + directory = currentDirectoryFilter, + displayDirectory = currentDirectoryFilter.toNativeSeparators(), torrentsCount = 0 ) } } TremotesfComboBox( - currentItem = currentDirectoryFilter::value, + currentItem = currentDirectoryFilterCalculated::value, updateCurrentItem = { sortAndFilterSettings.setDirectoryFilter(it.directory) }, items = calculatedFilters.directories, itemDisplayString = { if (it.directory.isEmpty()) { stringResource(R.string.torrents_all, it.torrentsCount) } else { - stringResource(R.string.directories_spinner_text, it.directory, it.torrentsCount) + stringResource(R.string.directories_spinner_text, it.displayDirectory, it.torrentsCount) } }, label = R.string.directories, @@ -293,7 +295,7 @@ private data class CalculatedFilters( ) { data class LabelFilter(val label: String, val torrentsCount: Int) data class TrackerFilter(val trackerSite: String, val torrentsCount: Int) - data class DirectoryFilter(val directory: String, val torrentsCount: Int) + data class DirectoryFilter(val directory: NormalizedRpcPath, val displayDirectory: String, val torrentsCount: Int) } private fun calculateFilters( @@ -308,7 +310,7 @@ private fun calculateFilters( null } val trackers = sortedMapOf(comparator, "" to torrents.size) - val directories = sortedMapOf(nullsFirst(compareBy(comparator, NormalizedRpcPath::value)), null to torrents.size) + val directories = sortedMapOf(compareBy(comparator, NormalizedRpcPath::value), NormalizedRpcPath.EMPTY to torrents.size) for (torrent in torrents) { for (mode in STATUS_FILTER_MODES_WITHOUT_ALL) { if (statusFilterAcceptsTorrent(torrent, mode)) { @@ -331,7 +333,8 @@ private fun calculateFilters( trackers = trackers.map { CalculatedFilters.TrackerFilter(it.key, it.value) }, directories = directories.map { CalculatedFilters.DirectoryFilter( - directory = it.key?.toNativeSeparators().orEmpty(), + directory = it.key, + displayDirectory = it.key.toNativeSeparators(), torrentsCount = it.value ) } @@ -358,7 +361,7 @@ private fun FiltersBottomSheetPreview() = ComponentPreview { statusFilterMode = MutableStateFlow(StatusFilterMode.Downloading), labelFilter = MutableStateFlow(""), trackerFilter = MutableStateFlow(""), - directoryFilter = MutableStateFlow(""), + directoryFilter = MutableStateFlow("".normalizePath(null)), isAnySettingChanged = MutableStateFlow(true) ) }, diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/TorrentsListFragmentViewModel.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/TorrentsListFragmentViewModel.kt index 5e82a8c2..388b497c 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/TorrentsListFragmentViewModel.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/TorrentsListFragmentViewModel.kt @@ -47,8 +47,10 @@ import org.equeim.tremotesf.rpc.RpcClient import org.equeim.tremotesf.rpc.RpcRequestError import org.equeim.tremotesf.rpc.RpcRequestState import org.equeim.tremotesf.rpc.Server +import org.equeim.tremotesf.rpc.normalizePath import org.equeim.tremotesf.rpc.performPeriodicRequest import org.equeim.tremotesf.rpc.performRecoveringRequestIntoStateFlow +import org.equeim.tremotesf.rpc.requests.NormalizedRpcPath import org.equeim.tremotesf.rpc.requests.Torrent import org.equeim.tremotesf.rpc.requests.TorrentStatus import org.equeim.tremotesf.rpc.requests.TransferRate @@ -165,7 +167,7 @@ class TorrentsListFragmentViewModel(application: Application, savedStateHandle: val statusFilterMode: StateFlow, val labelFilter: StateFlow, val trackerFilter: StateFlow, - val directoryFilter: StateFlow, + val directoryFilter: StateFlow, val isAnySettingChanged: StateFlow ) { fun setSortMode(mode: SortMode) { @@ -188,8 +190,8 @@ class TorrentsListFragmentViewModel(application: Application, savedStateHandle: GlobalScope.launch { Settings.torrentsTrackerFilter.set(tracker) } } - fun setDirectoryFilter(directory: String) { - GlobalScope.launch { Settings.torrentsDirectoryFilter.set(directory) } + fun setDirectoryFilter(directory: NormalizedRpcPath?) { + GlobalScope.launch { Settings.torrentsDirectoryFilter.set(directory?.value.orEmpty()) } } fun reset() { @@ -199,7 +201,7 @@ class TorrentsListFragmentViewModel(application: Application, savedStateHandle: setStatusFilterMode(StatusFilterMode.DEFAULT) setLabelFilter("") setTrackerFilter("") - setDirectoryFilter("") + setDirectoryFilter(null) } } @@ -216,7 +218,11 @@ class TorrentsListFragmentViewModel(application: Application, savedStateHandle: statusFilterMode = Settings.torrentsStatusFilter.flow().stateIn(viewModelScope), labelFilter = Settings.torrentsLabelFilter.flow().stateIn(viewModelScope), trackerFilter = Settings.torrentsTrackerFilter.flow().stateIn(viewModelScope), - directoryFilter = Settings.torrentsDirectoryFilter.flow().stateIn(viewModelScope), + directoryFilter = combine( + Settings.torrentsDirectoryFilter.flow(), + GlobalRpcClient.serverCapabilitiesFlow, + String::normalizePath + ).stateIn(viewModelScope), isAnySettingChanged = combine( nameFilterFlow, Settings.torrentsSortMode.flow(), @@ -421,14 +427,14 @@ class TorrentsListFragmentViewModel(application: Application, savedStateHandle: statusFilterMode: StatusFilterMode, labelFilter: String?, trackerFilter: String, - directoryFilter: String, + directoryFilter: NormalizedRpcPath, ): (Torrent) -> Boolean { return { torrent: Torrent -> (nameFilter.isEmpty() || torrent.name.contains(nameFilter, true)) && statusFilterAcceptsTorrent(torrent, statusFilterMode) && (labelFilter.isNullOrEmpty() || torrent.labels.contains(labelFilter)) && (trackerFilter.isEmpty() || (torrent.trackerSites.contains(trackerFilter))) && - (directoryFilter.isEmpty() || torrent.downloadDirectory.value == directoryFilter) + (directoryFilter.isEmpty() || torrent.downloadDirectory == directoryFilter) } } diff --git a/rpc/src/main/kotlin/org/equeim/tremotesf/rpc/Paths.kt b/rpc/src/main/kotlin/org/equeim/tremotesf/rpc/Paths.kt index 123946cd..ab2b813b 100644 --- a/rpc/src/main/kotlin/org/equeim/tremotesf/rpc/Paths.kt +++ b/rpc/src/main/kotlin/org/equeim/tremotesf/rpc/Paths.kt @@ -21,15 +21,12 @@ fun NormalizedRpcPath.toNativeSeparators(): String = @JvmName("normalizePathImpl") private fun normalizePath(path: String, serverCapabilities: ServerCapabilities?): NormalizedRpcPath { - //Timber.d("Normalizing path $path") if (path.isEmpty()) { - //Timber.d("Empty") - return NormalizedRpcPath(path, null) + return NormalizedRpcPath.EMPTY } var normalized = path.trim() if (normalized.isEmpty()) { - //Timber.d("Blank") - return NormalizedRpcPath(normalized, null) + return NormalizedRpcPath.EMPTY } if (serverCapabilities == null) { return NormalizedRpcPath(normalized, null) @@ -41,7 +38,6 @@ private fun normalizePath(path: String, serverCapabilities: ServerCapabilities?) } } normalized = normalized.collapseRepeatingSeparators(serverCapabilities).dropTrailingSeparator(serverCapabilities) - //Timber.d("Normalized to $normalized") return NormalizedRpcPath(normalized, serverCapabilities.serverOs) } diff --git a/rpc/src/main/kotlin/org/equeim/tremotesf/rpc/requests/Types.kt b/rpc/src/main/kotlin/org/equeim/tremotesf/rpc/requests/Types.kt index 44d2fef3..09345ddb 100644 --- a/rpc/src/main/kotlin/org/equeim/tremotesf/rpc/requests/Types.kt +++ b/rpc/src/main/kotlin/org/equeim/tremotesf/rpc/requests/Types.kt @@ -48,6 +48,8 @@ data class NormalizedRpcPath internal constructor( val value: String, internal val serverOs: ServerCapabilities.ServerOs?, ) { + fun isEmpty(): Boolean = value.isEmpty() + internal class Serializer(private val serverCapabilities: () -> ServerCapabilities?) : KSerializer { override val descriptor: SerialDescriptor = @@ -58,6 +60,10 @@ data class NormalizedRpcPath internal constructor( override fun serialize(encoder: Encoder, value: NormalizedRpcPath) = encoder.encodeString(value.value) } + + companion object { + val EMPTY = NormalizedRpcPath(value = "", serverOs = null) + } } @JvmInline From 06db3a03da77e04a9e33a158ddddd2ec229725fc Mon Sep 17 00:00:00 2001 From: Alexey Rochev Date: Tue, 11 Aug 2026 03:15:21 +0300 Subject: [PATCH 6/7] Use "since last connection" settings for notifications when switching between servers --- .../org/equeim/tremotesf/rpc/PeriodicServerStateUpdater.kt | 6 ++---- rpc/src/main/kotlin/org/equeim/tremotesf/rpc/RpcClient.kt | 7 +++++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/src/main/kotlin/org/equeim/tremotesf/rpc/PeriodicServerStateUpdater.kt b/app/src/main/kotlin/org/equeim/tremotesf/rpc/PeriodicServerStateUpdater.kt index 5eaafbd6..f843f921 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/rpc/PeriodicServerStateUpdater.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/rpc/PeriodicServerStateUpdater.kt @@ -76,10 +76,8 @@ object PeriodicServerStateUpdater { } coroutineScope.launch { - GlobalRpcClient.shouldConnectToServer.collect { - if (!it) { - updatedTorrentsSinceEnablingConnection.set(false) - } + GlobalRpcClient.disconnectedFromServer.collect { + updatedTorrentsSinceEnablingConnection.set(false) } } diff --git a/rpc/src/main/kotlin/org/equeim/tremotesf/rpc/RpcClient.kt b/rpc/src/main/kotlin/org/equeim/tremotesf/rpc/RpcClient.kt index c03fada1..dd33d1c8 100644 --- a/rpc/src/main/kotlin/org/equeim/tremotesf/rpc/RpcClient.kt +++ b/rpc/src/main/kotlin/org/equeim/tremotesf/rpc/RpcClient.kt @@ -5,7 +5,9 @@ package org.equeim.tremotesf.rpc import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.distinctUntilChanged @@ -59,6 +61,9 @@ open class RpcClient( val shouldConnectToServer = MutableStateFlow(true) + val disconnectedFromServer: Flow + field = MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + init { coroutineScope.launch { shouldConnectToServer.collect { @@ -66,6 +71,7 @@ open class RpcClient( connectionConfiguration.value?.getOrNull()?.httpClient?.apply { dispatcher.cancelAll() connectionPool.evictAll() + disconnectedFromServer.tryEmit(Unit) } } } @@ -87,6 +93,7 @@ open class RpcClient( dispatcher.cancelAll() dispatcher.executorService.shutdown() connectionPool.evictAll() + disconnectedFromServer.tryEmit(Unit) } sessionId = null serverCapabilitiesResult.value = null From a10850d87c52c44766779ec91992b490f59139b4 Mon Sep 17 00:00:00 2001 From: Alexey Rochev Date: Wed, 12 Aug 2026 00:21:11 +0300 Subject: [PATCH 7/7] Use explicit backing field for connectionConfiguration --- rpc/src/main/kotlin/org/equeim/tremotesf/rpc/RpcClient.kt | 4 ++-- .../main/kotlin/org/equeim/tremotesf/rpc/RpcRequestError.kt | 2 +- .../main/kotlin/org/equeim/tremotesf/rpc/RpcRequestState.kt | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/rpc/src/main/kotlin/org/equeim/tremotesf/rpc/RpcClient.kt b/rpc/src/main/kotlin/org/equeim/tremotesf/rpc/RpcClient.kt index dd33d1c8..f5daf9aa 100644 --- a/rpc/src/main/kotlin/org/equeim/tremotesf/rpc/RpcClient.kt +++ b/rpc/src/main/kotlin/org/equeim/tremotesf/rpc/RpcClient.kt @@ -38,8 +38,8 @@ open class RpcClient( protected val coroutineScope: CoroutineScope, private val retryOnConnectionFailure: Boolean = true ) { - private val connectionConfiguration = MutableStateFlow?>(null) - internal fun getConnectionConfiguration(): StateFlow?> = connectionConfiguration + internal val connectionConfiguration: StateFlow?> + field = MutableStateFlow(null) internal val json = Json { ignoreUnknownKeys = true diff --git a/rpc/src/main/kotlin/org/equeim/tremotesf/rpc/RpcRequestError.kt b/rpc/src/main/kotlin/org/equeim/tremotesf/rpc/RpcRequestError.kt index 0b9f351b..30b7cbf3 100644 --- a/rpc/src/main/kotlin/org/equeim/tremotesf/rpc/RpcRequestError.kt +++ b/rpc/src/main/kotlin/org/equeim/tremotesf/rpc/RpcRequestError.kt @@ -210,7 +210,7 @@ fun RpcRequestError.makeDetailedError(client: RpcClient): DetailedRpcRequestErro clientCertificates = response?.run { withPriorResponses.flatMap { it.handshake?.localCertificates.orEmpty() }.toSet().toList() } - ?: client.getConnectionConfiguration().value?.getOrNull()?.clientCertificates.orEmpty(), + ?: client.connectionConfiguration.value?.getOrNull()?.clientCertificates.orEmpty(), requestHeaders = requestHeaders?.toList().orEmpty(), ) } diff --git a/rpc/src/main/kotlin/org/equeim/tremotesf/rpc/RpcRequestState.kt b/rpc/src/main/kotlin/org/equeim/tremotesf/rpc/RpcRequestState.kt index b7d4c643..33cd95ac 100644 --- a/rpc/src/main/kotlin/org/equeim/tremotesf/rpc/RpcRequestState.kt +++ b/rpc/src/main/kotlin/org/equeim/tremotesf/rpc/RpcRequestState.kt @@ -83,7 +83,7 @@ fun RpcClient.performPeriodicRequest( } while (currentCoroutineContext().isActive) { actuallyPerformRecoveringRequest(performRequest, this) - val updateInterval = getConnectionConfiguration().value?.getOrNull()?.updateInterval + val updateInterval = connectionConfiguration.value?.getOrNull()?.updateInterval if (updateInterval != null) { delay(updateInterval) } else { @@ -167,7 +167,7 @@ private suspend fun RpcClient.actuallyPerformRecoveringRequest( private class StartRequestFromScratch(val nonRecoverableError: RpcRequestState.Error? = null) private fun RpcClient.startRequestFromScratchEvents(): Flow = combine( - getConnectionConfiguration(), + connectionConfiguration, shouldConnectToServer ) { configuration, shouldConnectToServer -> StartRequestFromScratch(getInitialNonRecoverableError(configuration, shouldConnectToServer)) @@ -185,7 +185,7 @@ private fun getInitialNonRecoverableError( } private fun RpcClient.getInitialRequestState(): RpcRequestState = getInitialNonRecoverableError( - getConnectionConfiguration().value, + connectionConfiguration.value, shouldConnectToServer.value ) ?: RpcRequestState.Loading