Skip to content
Merged

Fixes #383

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
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,8 @@ object PeriodicServerStateUpdater {
}

coroutineScope.launch {
GlobalRpcClient.shouldConnectToServer.collect {
if (!it) {
updatedTorrentsSinceEnablingConnection.set(false)
}
GlobalRpcClient.disconnectedFromServer.collect {
updatedTorrentsSinceEnablingConnection.set(false)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,6 @@ class AddTorrentFileModelImpl(
infoHashV1 = parseResult.infoHashV1
trackers = parseResult.trackers

checkIfTorrentExists()
if (checkIfTorrentExists()) {
return@withContext LoadingState.Aborted
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ 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
import org.equeim.tremotesf.ui.ComponentPreview
Expand Down Expand Up @@ -176,49 +178,71 @@ 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,
modifier = Modifier.fillMaxWidth()
)
}

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 currentDirectoryFilter by sortAndFilterSettings.directoryFilter.collectAsStateWithLifecycle()
val currentDirectoryFilterCalculated = remember {
derivedStateOf {
calculatedFilters.directories.find { it.directory == currentDirectoryFilter }
?: CalculatedFilters.DirectoryFilter(
directory = currentDirectoryFilter,
displayDirectory = currentDirectoryFilter.toNativeSeparators(),
torrentsCount = 0
)
}
}
TremotesfComboBox(
currentItem = sortAndFilterSettings.directoryFilter.collectAsStateWithLifecycle()::value,
updateCurrentItem = sortAndFilterSettings::setDirectoryFilter,
items = calculatedFilters.sortedDirectories,
currentItem = currentDirectoryFilterCalculated::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.displayDirectory, it.torrentsCount)
}
},
label = R.string.directories,
Expand Down Expand Up @@ -265,13 +289,14 @@ private fun SortOrderButtons(

private data class CalculatedFilters(
val statusFilterModesCounts: Map<StatusFilterMode, Int>,
val sortedLabels: List<String>,
val labelsCounts: Map<String, Int>,
val sortedTrackers: List<String>,
val trackersCounts: Map<String, Int>,
val sortedDirectories: List<String>,
val directoriesCounts: Map<String, Int>
)
val labels: List<LabelFilter>,
val trackers: List<TrackerFilter>,
val directories: List<DirectoryFilter>
) {
data class LabelFilter(val label: String, val torrentsCount: Int)
data class TrackerFilter(val trackerSite: String, val torrentsCount: Int)
data class DirectoryFilter(val directory: NormalizedRpcPath, val displayDirectory: String, val torrentsCount: Int)
}

private fun calculateFilters(
torrents: List<Torrent>,
Expand All @@ -280,12 +305,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(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)) {
Expand All @@ -300,21 +325,24 @@ 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,
displayDirectory = it.key.toNativeSeparators(),
torrentsCount = it.value
)
}
)
}

private object IncrementCount : BiFunction<Any, Int?, Int> {
override fun apply(key: Any, count: Int?): Int {
private object IncrementCount : BiFunction<Any?, Int?, Int> {
override fun apply(key: Any?, count: Int?): Int {
return (count ?: 0) + 1
}
}
Expand All @@ -333,7 +361,7 @@ private fun FiltersBottomSheetPreview() = ComponentPreview {
statusFilterMode = MutableStateFlow(StatusFilterMode.Downloading),
labelFilter = MutableStateFlow(""),
trackerFilter = MutableStateFlow(""),
directoryFilter = MutableStateFlow(""),
directoryFilter = MutableStateFlow("".normalizePath(null)),
isAnySettingChanged = MutableStateFlow(true)
)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -165,7 +167,7 @@ class TorrentsListFragmentViewModel(application: Application, savedStateHandle:
val statusFilterMode: StateFlow<StatusFilterMode>,
val labelFilter: StateFlow<String>,
val trackerFilter: StateFlow<String>,
val directoryFilter: StateFlow<String>,
val directoryFilter: StateFlow<NormalizedRpcPath>,
val isAnySettingChanged: StateFlow<Boolean>
) {
fun setSortMode(mode: SortMode) {
Expand All @@ -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() {
Expand All @@ -199,7 +201,7 @@ class TorrentsListFragmentViewModel(application: Application, savedStateHandle:
setStatusFilterMode(StatusFilterMode.DEFAULT)
setLabelFilter("")
setTrackerFilter("")
setDirectoryFilter("")
setDirectoryFilter(null)
}
}

Expand All @@ -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<Any, Boolean>(
nameFilterFlow,
Settings.torrentsSortMode.flow(),
Expand Down Expand Up @@ -356,16 +362,16 @@ class TorrentsListFragmentViewModel(application: Application, savedStateHandle:

val torrentOperations: TorrentsOperations = object : TorrentsOperations {
override fun start(ids: Set<Int>) =
performRequestAndRefresh(R.string.torrents_reannounce_error) { startTorrents(ids) }
performRequestAndRefresh(R.string.torrents_start_error) { startTorrents(ids) }

override fun startNow(ids: Set<Int>) =
performRequestAndRefresh(R.string.torrents_reannounce_error) { startTorrentsNow(ids) }
performRequestAndRefresh(R.string.torrents_start_error) { startTorrentsNow(ids) }

override fun stop(ids: Set<Int>) =
performRequestAndRefresh(R.string.torrents_reannounce_error) { stopTorrents(ids) }
performRequestAndRefresh(R.string.torrents_pause_error) { stopTorrents(ids) }

override fun verify(ids: Set<Int>) =
performRequestAndRefresh(R.string.torrents_reannounce_error) { verifyTorrents(ids) }
performRequestAndRefresh(R.string.torrents_check_error) { verifyTorrents(ids) }

override fun reannounce(ids: Set<Int>) =
performRequestAndRefresh(R.string.torrents_reannounce_error) { reannounceTorrents(ids) }
Expand Down Expand Up @@ -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)
}
}

Expand Down
8 changes: 2 additions & 6 deletions rpc/src/main/kotlin/org/equeim/tremotesf/rpc/Paths.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
}

Expand Down
11 changes: 9 additions & 2 deletions rpc/src/main/kotlin/org/equeim/tremotesf/rpc/RpcClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -36,8 +38,8 @@ open class RpcClient(
protected val coroutineScope: CoroutineScope,
private val retryOnConnectionFailure: Boolean = true
) {
private val connectionConfiguration = MutableStateFlow<Result<ConnectionConfiguration>?>(null)
internal fun getConnectionConfiguration(): StateFlow<Result<ConnectionConfiguration>?> = connectionConfiguration
internal val connectionConfiguration: StateFlow<Result<ConnectionConfiguration>?>
field = MutableStateFlow(null)

internal val json = Json {
ignoreUnknownKeys = true
Expand All @@ -59,13 +61,17 @@ open class RpcClient(

val shouldConnectToServer = MutableStateFlow(true)

val disconnectedFromServer: Flow<Unit>
field = MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)

init {
coroutineScope.launch {
shouldConnectToServer.collect {
if (!it) {
connectionConfiguration.value?.getOrNull()?.httpClient?.apply {
dispatcher.cancelAll()
connectionPool.evictAll()
disconnectedFromServer.tryEmit(Unit)
}
}
}
Expand All @@ -87,6 +93,7 @@ open class RpcClient(
dispatcher.cancelAll()
dispatcher.executorService.shutdown()
connectionPool.evictAll()
disconnectedFromServer.tryEmit(Unit)
}
sessionId = null
serverCapabilitiesResult.value = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
)
}
Expand Down
Loading