diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e0726b519..b10d60fa3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -8,7 +8,6 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.plugin.compose) alias(libs.plugins.kotlin.plugin.parcelize) - alias(libs.plugins.androidx.navigation) alias(libs.plugins.tremotesf.common.settings) } @@ -91,12 +90,12 @@ dependencies { implementation(libs.androidx.activity) implementation(libs.androidx.annotation) implementation(libs.androidx.core) - implementation(libs.androidx.fragment) implementation(libs.androidx.lifecycle.runtime) implementation(libs.androidx.lifecycle.viewmodel) implementation(libs.androidx.lifecycle.viewmodel.savedstate) implementation(libs.androidx.lifecycle.service) - implementation(libs.androidx.navigation.fragment) + implementation(libs.androidx.navigation3.runtime) + implementation(libs.androidx.navigation3.ui) implementation(libs.androidx.webkit) implementation(libs.androidx.window) implementation(libs.androidx.work.runtime) diff --git a/app/src/main/kotlin/org/equeim/tremotesf/service/NotificationsController.kt b/app/src/main/kotlin/org/equeim/tremotesf/service/NotificationsController.kt index aa377350b..80eda8e09 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/service/NotificationsController.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/service/NotificationsController.kt @@ -7,11 +7,12 @@ package org.equeim.tremotesf.service import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager +import android.app.PendingIntent import android.content.Context +import android.content.Intent import android.graphics.drawable.Icon import androidx.annotation.StringRes import androidx.core.content.getSystemService -import androidx.navigation.NavDeepLinkBuilder import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import org.equeim.tremotesf.R @@ -20,8 +21,10 @@ import org.equeim.tremotesf.rpc.RpcRequestState import org.equeim.tremotesf.rpc.Server import org.equeim.tremotesf.rpc.getErrorString import org.equeim.tremotesf.rpc.requests.SessionStatsResponseArguments +import org.equeim.tremotesf.ui.NavigationActivity import org.equeim.tremotesf.ui.Settings -import org.equeim.tremotesf.ui.torrentpropertiesfragment.TorrentPropertiesFragmentArgs +import org.equeim.tremotesf.ui.toInternalDeepLink +import org.equeim.tremotesf.ui.torrentproperties.TorrentPropertiesDestination import org.equeim.tremotesf.ui.utils.FileSizeFormatter import org.equeim.tremotesf.ui.utils.localeChangedEvents import timber.log.Timber @@ -114,11 +117,14 @@ class NotificationsController(private val context: Context, coroutineScope: Coro .setContentTitle(context.getText(notificationTitle)) .setContentText(torrentName) .setContentIntent( - NavDeepLinkBuilder(context) - .setGraph(R.navigation.nav_main) - .setDestination(R.id.torrent_properties_fragment) - .setArguments(TorrentPropertiesFragmentArgs(hashString).toBundle()) - .createPendingIntent() + PendingIntent.getActivity( + context, + 0, + Intent(context, NavigationActivity::class.java) + .setData(TorrentPropertiesDestination(hashString).toInternalDeepLink()) + .setAction(Intent.ACTION_VIEW), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) ) .setAutoCancel(true) .build() @@ -133,10 +139,12 @@ class NotificationsController(private val context: Context, coroutineScope: Coro Notification.Builder(context, PERSISTENT_NOTIFICATION_CHANNEL_ID) .setSmallIcon(R.drawable.notification_icon) .setContentIntent( - NavDeepLinkBuilder(context) - .setGraph(R.navigation.nav_main) - .setDestination(R.id.torrents_list_fragment) - .createPendingIntent() + PendingIntent.getActivity( + context, + 0, + Intent(context, NavigationActivity::class.java), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) ) .setOngoing(true) .setShowWhen(false) diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/AboutFragment.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/AboutScreen.kt similarity index 97% rename from app/src/main/kotlin/org/equeim/tremotesf/ui/AboutFragment.kt rename to app/src/main/kotlin/org/equeim/tremotesf/ui/AboutScreen.kt index 35c9c4c30..565016acf 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/AboutFragment.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/AboutScreen.kt @@ -55,23 +55,24 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.viewinterop.AndroidView -import androidx.navigation.NavController import androidx.webkit.WebSettingsCompat import androidx.webkit.WebViewFeature import kotlinx.coroutines.launch +import kotlinx.parcelize.Parcelize import org.equeim.tremotesf.BuildConfig import org.equeim.tremotesf.R import org.equeim.tremotesf.ui.components.TremotesfTopAppBar import org.equeim.tremotesf.ui.components.TremotesfTopAppBarDefaults -import org.equeim.tremotesf.ui.utils.safeNavigate import timber.log.Timber -class AboutFragment : ComposeFragment() { +@Parcelize +data object AboutDestination : Destination { @Composable override fun Content(navController: NavController) { AboutScreen( - navigateUp = navController::navigateUp, - showLicense = { navController.safeNavigate(AboutFragmentDirections.toLicenseFragment()) }) + navigateUp = navController::popBackStack, + showLicense = { navController.navigateTo(LicenseDestination) } + ) } } @@ -284,14 +285,14 @@ private fun TranslatorsTab(innerPadding: PaddingValues) { } } -class LicenceFragment : ComposeFragment() { +@Parcelize +data object LicenseDestination : Destination { @Composable override fun Content(navController: NavController) { - LicenseScreen(navController::navigateUp) + LicenseScreen(navController::popBackStack) } } -@OptIn(ExperimentalMaterial3Api::class) @Composable private fun LicenseScreen(navigateUp: () -> Unit) { Scaffold( diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/ComposeDialogFragment.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/ComposeDialogFragment.kt deleted file mode 100644 index 0c277f76d..000000000 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/ComposeDialogFragment.kt +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-FileCopyrightText: 2017-2026 Alexey Rochev -// -// SPDX-License-Identifier: GPL-3.0-or-later - -package org.equeim.tremotesf.ui - -import android.app.Dialog -import android.os.Bundle -import android.view.Gravity -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.fragment.app.DialogFragment -import androidx.navigation.NavController -import androidx.navigation.fragment.findNavController - -abstract class ComposeDialogFragment : DialogFragment() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setStyle(STYLE_NO_TITLE, 0) - } - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - return super.onCreateDialog(savedInstanceState).apply { - window?.apply { - setBackgroundDrawableResource(android.R.color.transparent) - setGravity(Gravity.CENTER) - } - } - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - return ComposeView(requireContext()).apply { - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnLifecycleDestroyed(this@ComposeDialogFragment)) - val navController = findNavController() - setContent { - ApplicationTheme { - Content(navController) - } - } - } - } - - @Composable - abstract fun Content(navController: NavController) -} diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/ComposeFragment.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/ComposeFragment.kt deleted file mode 100644 index 5fe438843..000000000 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/ComposeFragment.kt +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-FileCopyrightText: 2017-2026 Alexey Rochev -// -// SPDX-License-Identifier: GPL-3.0-or-later - -package org.equeim.tremotesf.ui - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.fragment.app.Fragment -import androidx.navigation.NavController -import androidx.navigation.fragment.findNavController - -abstract class ComposeFragment : Fragment() { - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - return ComposeView(requireContext()).apply { - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - val navController = findNavController() - setContent { - ApplicationTheme { - Content(navController) - } - } - } - } - - @Composable - abstract fun Content(navController: NavController) -} diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/DetailedConnectionErrorDialogFragment.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/DetailedConnectionErrorDialogScreen.kt similarity index 100% rename from app/src/main/kotlin/org/equeim/tremotesf/ui/DetailedConnectionErrorDialogFragment.kt rename to app/src/main/kotlin/org/equeim/tremotesf/ui/DetailedConnectionErrorDialogScreen.kt diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/ExtendedViewModelStoreNavEntryDecorator.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/ExtendedViewModelStoreNavEntryDecorator.kt new file mode 100644 index 000000000..dd45f5626 --- /dev/null +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/ExtendedViewModelStoreNavEntryDecorator.kt @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: 2017-2026 Alexey Rochev +// +// SPDX-License-Identifier: GPL-3.0-or-later + +package org.equeim.tremotesf.ui + +import android.app.Application +import androidx.activity.compose.LocalActivity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelStoreOwner +import androidx.lifecycle.viewmodel.MutableCreationExtras +import androidx.lifecycle.viewmodel.ViewModelStoreProvider +import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner +import androidx.lifecycle.viewmodel.compose.rememberViewModelStoreOwner +import androidx.navigation3.runtime.NavEntryDecorator +import androidx.savedstate.compose.LocalSavedStateRegistryOwner + +@Composable +fun rememberExtendedViewModelStoreNavEntryDecorator( + viewModelStoreOwner: ViewModelStoreOwner = + checkNotNull(LocalViewModelStoreOwner.current) { + "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner" + } +): ExtendedViewModelStoreNavEntryDecorator { + val application = checkNotNull(LocalActivity.current).application + return remember(viewModelStoreOwner) { + ExtendedViewModelStoreNavEntryDecorator( + viewModelStoreOwner = viewModelStoreOwner, + application = application + ) + } +} + +class ExtendedViewModelStoreNavEntryDecorator(private val viewModelStoreProvider: ViewModelStoreProvider) : + NavEntryDecorator( + onPop = viewModelStoreProvider::clearKey, + decorate = { entry -> + val childViewModelStoreOwner = rememberViewModelStoreOwner( + provider = viewModelStoreProvider, + savedStateRegistryOwner = LocalSavedStateRegistryOwner.current, + key = entry.contentKey, + ) + CompositionLocalProvider(LocalViewModelStoreOwner provides childViewModelStoreOwner) { + entry.Content() + } + }, + ) { + + constructor( + viewModelStoreOwner: ViewModelStoreOwner, + application: Application, + ) : this( + viewModelStoreProvider = ViewModelStoreProvider( + parentStore = viewModelStoreOwner.viewModelStore, + parentKey = ExtendedViewModelStoreNavEntryDecorator::class.qualifiedName, + defaultCreationExtras = MutableCreationExtras().also { + it[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] = application + }) + ) + + @Composable + fun viewModelStoreOwnerForKey(contentKey: Any): ViewModelStoreOwner { + return rememberViewModelStoreOwner( + provider = viewModelStoreProvider, + savedStateRegistryOwner = LocalSavedStateRegistryOwner.current, + key = contentKey + ) + } +} diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/LabelsEditDialogFragment.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/LabelsEditDialogScreen.kt similarity index 85% rename from app/src/main/kotlin/org/equeim/tremotesf/ui/LabelsEditDialogFragment.kt rename to app/src/main/kotlin/org/equeim/tremotesf/ui/LabelsEditDialogScreen.kt index f30922f01..de3539166 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/LabelsEditDialogFragment.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/LabelsEditDialogScreen.kt @@ -15,11 +15,14 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.ViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavController +import androidx.navigation3.runtime.metadata +import androidx.navigation3.scene.DialogSceneStrategy +import androidx.navigation3.scene.DialogSceneStrategy.Companion.DialogKey import kotlinx.coroutines.flow.StateFlow import org.equeim.tremotesf.R import org.equeim.tremotesf.rpc.GlobalRpcClient @@ -33,19 +36,26 @@ import org.equeim.tremotesf.ui.components.TremotesfScreenContentWithPlaceholder import org.equeim.tremotesf.ui.components.rememberTremotesfInitialFocusRequester import org.equeim.tremotesf.ui.utils.SnapshotStateListSaver import org.equeim.tremotesf.ui.utils.rememberAlphanumericComparator +import kotlinx.parcelize.Parcelize -class LabelsEditDialogFragment : ComposeDialogFragment() { +@Parcelize +data class LabelsEditDialogDestination( + val torrentHashStrings: List, + val enabledLabels: List +) : Destination { @Composable override fun Content(navController: NavController) { - val args = LabelsEditDialogFragmentArgs.fromBundle(requireArguments()) - val model = viewModel { LabelsEditDialogViewModel(args.torrentHashStrings.asList()) } + val model = viewModel { LabelsEditDialogViewModel(torrentHashStrings) } LabelsEditDialogContent( - initialEnabledLabels = args::enabledLabels, + initialEnabledLabels = { enabledLabels.toTypedArray() }, allLabels = model.allLabels.collectAsStateWithLifecycle(), updateLabels = model::updateLabels, - onDismissRequest = ::dismiss, + onDismissRequest = navController::popBackStack ) } + + override val metadata: Map + get() = metadata { put(DialogKey, DialogProperties()) } } @Composable diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/NavController.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/NavController.kt new file mode 100644 index 000000000..a28d2021b --- /dev/null +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/NavController.kt @@ -0,0 +1,109 @@ +// SPDX-FileCopyrightText: 2017-2026 Alexey Rochev +// +// SPDX-License-Identifier: GPL-3.0-or-later + +package org.equeim.tremotesf.ui + +import android.os.ParcelUuid +import android.os.Parcelable +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.lifecycle.ViewModelStoreOwner +import kotlinx.parcelize.Parcelize +import org.equeim.tremotesf.ui.NavController.BackStackEntry +import java.util.UUID + +interface Destination : Parcelable { + @Composable + fun Content(navController: NavController) + + val metadata: Map + get() = emptyMap() +} + +@Stable +class NavController( + initialBackStack: List, + private val viewModelStoreDecorator: ExtendedViewModelStoreNavEntryDecorator +) { + val backStack: List + field = mutableStateListOf().apply { addAll(initialBackStack) } + + fun navigateTo(destination: Destination) { + backStack.add(BackStackEntry(destination)) + } + + fun popBackStack() { + backStack.removeLastOrNull() + } + + fun popUpTo(destinationClass: Class) { + val index = backStack.indexOfLast { destinationClass.isInstance(it.destination) } + if (index in 0.. popUpTo() { + popUpTo(T::class.java) + } + + fun resetFirstDestination(destination: Destination) { + if (backStack.size > 1) { + backStack.removeRange(1, backStack.size) + } + backStack[0] = BackStackEntry(destination) + } + + @Composable + fun viewModelStoreOwnerForDestinationOrNull(destinationClass: Class): ViewModelStoreOwner? { + val backStackEntry = backStack.findLast { destinationClass.isInstance(it.destination) } ?: return null + return viewModelStoreDecorator.viewModelStoreOwnerForKey(backStackEntry.contentKey) + } + + @Composable + inline fun viewModelStoreOwnerForDestinationOrNull(): ViewModelStoreOwner? = + viewModelStoreOwnerForDestinationOrNull(T::class.java) + + @Composable + fun viewModelStoreOwnerForDestination(destinationClass: Class): ViewModelStoreOwner { + val owner = viewModelStoreOwnerForDestinationOrNull(destinationClass) + return checkNotNull(owner) { "Destination of type ${destinationClass.simpleName} does not exist on the back stack\nBack stack: $backStack" } + } + + @Composable + inline fun viewModelStoreOwnerForDestination(): ViewModelStoreOwner = + viewModelStoreOwnerForDestination(T::class.java) + + @Parcelize + data class BackStackEntry( + val destination: Destination, + private val uuid: ParcelUuid = ParcelUuid(UUID.randomUUID()) + ) : Parcelable { + val contentKey: Any get() = uuid + } + + companion object { + fun Saver(viewModelStoreDecorator: ExtendedViewModelStoreNavEntryDecorator) = + Saver>( + save = { it.backStack }, + restore = { NavController(initialBackStack = it, viewModelStoreDecorator = viewModelStoreDecorator) } + ) + } +} + +@Composable +fun rememberNavController( + initialDestinations: List, + viewModelStoreDecorator: ExtendedViewModelStoreNavEntryDecorator +): NavController { + return rememberSaveable(saver = NavController.Saver(viewModelStoreDecorator)) { + NavController( + initialBackStack = initialDestinations.map(::BackStackEntry), + viewModelStoreDecorator = viewModelStoreDecorator + ) + } +} diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/NavigationActivity.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/NavigationActivity.kt index a8e7c173b..9871fc4d5 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/NavigationActivity.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/NavigationActivity.kt @@ -10,34 +10,35 @@ import android.content.Intent import android.content.res.Configuration import android.os.Build import android.os.Bundle -import android.view.DragEvent -import android.view.LayoutInflater +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent import androidx.activity.viewModels -import androidx.annotation.AnimatorRes -import androidx.annotation.IdRes +import androidx.compose.foundation.draganddrop.dragAndDropTarget +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.draganddrop.DragAndDropEvent +import androidx.compose.ui.draganddrop.DragAndDropTarget +import androidx.compose.ui.draganddrop.toAndroidDragEvent import androidx.core.app.ActivityCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsControllerCompat -import androidx.fragment.app.FragmentActivity -import androidx.fragment.app.FragmentManager -import androidx.fragment.app.PredictiveBackControl import androidx.lifecycle.lifecycleScope -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import androidx.navigation.NavHostController -import androidx.navigation.NavOptions -import androidx.navigation.Navigator -import androidx.navigation.fragment.NavHostFragment +import androidx.navigation3.runtime.NavEntry +import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator +import androidx.navigation3.scene.DialogSceneStrategy +import androidx.navigation3.scene.SinglePaneSceneStrategy +import androidx.navigation3.ui.NavDisplay +import androidx.navigation3.ui.defaultPopTransitionSpec +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import org.equeim.tremotesf.R -import org.equeim.tremotesf.databinding.NavigationActivityBinding import org.equeim.tremotesf.service.ForegroundService -import org.equeim.tremotesf.ui.utils.hideKeyboard +import org.equeim.tremotesf.ui.torrentslist.TorrentsListDestination import timber.log.Timber -class NavigationActivity : FragmentActivity() { +class NavigationActivity : ComponentActivity() { companion object { private val createdActivities = mutableListOf() @@ -56,9 +57,7 @@ class NavigationActivity : FragmentActivity() { private val model by viewModels() - private lateinit var binding: NavigationActivityBinding - - private lateinit var navController: NavController + private val deepLinkDestinations = Channel(Channel.CONFLATED) private lateinit var initialDarkThemeMode: Settings.DarkThemeMode @@ -88,10 +87,6 @@ class NavigationActivity : FragmentActivity() { Timber.i("onCreate() called with: savedInstanceState = $savedInstanceState") Timber.i("onCreate: intent = $intent") - // https://issuetracker.google.com/issues/342919181 - @OptIn(PredictiveBackControl::class) - FragmentManager.enablePredictiveBack(false) - super.onCreate(savedInstanceState) createdActivities.add(this) AppForegroundTracker.registerActivity(this) @@ -99,17 +94,71 @@ class NavigationActivity : FragmentActivity() { WindowCompat.setDecorFitsSystemWindows(window, false) Timber.d("Night mode is ${resources.configuration.nightModeString()}") - overrideIntentWithDeepLink() - - binding = NavigationActivityBinding.inflate(LayoutInflater.from(this)) - setContentView(binding.root) + val initialDestinations = model.getInitialDestinations(intent, isTaskRoot) + Timber.d("Initial destinations = $initialDestinations") + setContent { + ApplicationTheme { + val viewModelStoreDecorator = + rememberExtendedViewModelStoreNavEntryDecorator() + val navController = rememberNavController( + initialDestinations = initialDestinations, + viewModelStoreDecorator = viewModelStoreDecorator + ) + + LaunchedEffect(Unit) { + for (destination in deepLinkDestinations) { + navController.apply { + if (isTaskRoot) { + popUpTo() + navigateTo(destination) + } else { + resetFirstDestination(destination) + } + } + } + } - navController = - (supportFragmentManager.findFragmentById(R.id.nav_host) as NavHostFragment).navController - navController.addOnDestinationChangedListener { _, _, _ -> - hideKeyboard() + NavDisplay( + backStack = navController.backStack, + onBack = navController::popBackStack, + sceneStrategies = listOf(DialogSceneStrategy(), SinglePaneSceneStrategy()), + entryProvider = { key -> + NavEntry( + key = key, + contentKey = key.contentKey, + metadata = key.destination.metadata + ) { key.destination.Content(navController) } + }, + entryDecorators = listOf( + rememberSaveableStateHolderNavEntryDecorator(), + viewModelStoreDecorator + ), + // Use the same animation as when navigation back though button on the toolbar to get rid of ugly scaling animation + // We need to do it like that because predictivePopTransitionSpec takes a parameter which we want to ignore + predictivePopTransitionSpec = defaultPopTransitionSpec() + .let { popSpec -> { popSpec() } }, + modifier = Modifier.dragAndDropTarget( + shouldStartDragAndDrop = model::shouldStartDragAndDrop, + target = object : DragAndDropTarget { + override fun onDrop(event: DragAndDropEvent): Boolean { + Timber.i("Received onDrop event") + val permissions = requestDragAndDropPermissions(event.toAndroidDragEvent()) + val destination = model.getAddTorrentDestination(event) + return if (destination != null) { + deepLinkDestinations.trySend(destination) + Timber.i("Accepting onDrop event") + true + } else { + Timber.i("Rejecting onDrop event") + permissions?.release() + false + } + } + } + ) + ) + } } - handleDropEvents() ForegroundService.startStopAutomatically() @@ -129,7 +178,7 @@ class NavigationActivity : FragmentActivity() { super.onConfigurationChanged(newConfig) Timber.d("onConfigurationChanged: night mode is ${newConfig.nightModeString()}") // These properties are set by Activity once on creation, so we need to update them ourselves on configuration change - WindowInsetsControllerCompat(window, binding.root).apply { + WindowInsetsControllerCompat(window, findViewById(android.R.id.content)).apply { val isLight = resources.getBoolean(R.bool.is_light_theme) isAppearanceLightStatusBars = isLight if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { @@ -143,52 +192,6 @@ class NavigationActivity : FragmentActivity() { } } - private fun overrideIntentWithDeepLink() { - if (model.navigatedInitially) return - model.navigatedInitially = true - - val intent = model.getInitialDeepLinkIntent(intent) ?: return - Timber.i("overrideIntentWithDeepLink: intent = $intent") - this.intent = intent - } - - private fun handleDropEvents() { - binding.root.setOnDragListener { _, event -> - when (event.action) { - DragEvent.ACTION_DRAG_STARTED -> { - Timber.d("Handling drag start event") - model.acceptDragStartEvent(event.clipDescription) - } - - DragEvent.ACTION_DROP -> { - Timber.d("Handling drop event") - val directions = model.getAddTorrentDirections(event.clipData) - if (directions != null) { - requestDragAndDropPermissions(event) - navController.navigate( - directions.destinationId, - directions.arguments, - NavOptions.Builder() - .setPopUpTo(navController.graph.startDestinationId, false) - .build() - ) - } - directions != null - } - /** - * Don't enter [also] branch to avoid log spam - */ - else -> return@setOnDragListener false - }.also { - if (it) { - Timber.d("Accepting event") - } else { - Timber.d("Rejecting event") - } - } - } - } - override fun onStart() { Timber.i("onStart() called") super.onStart() @@ -208,67 +211,6 @@ class NavigationActivity : FragmentActivity() { override fun onNewIntent(intent: Intent) { Timber.i("onNewIntent() called with: intent = $intent") super.onNewIntent(intent) - model.getAddTorrentDirections(intent)?.let { (destinationId, arguments) -> - navController.navigate( - destinationId, - arguments, - NavOptions.Builder() - .setPopUpTo(navController.graph.startDestinationId, false) - .build() - ) - } - } -} - -class NavHostFragment : NavHostFragment() { - override fun onCreateNavHostController(navHostController: NavHostController) { - super.onCreateNavHostController(navHostController) - navHostController.addOnDestinationChangedListener { _, destination, _ -> - Timber.i("Destination changed: destination = $destination") - } - } - - @Suppress("OVERRIDE_DEPRECATION") - override fun createFragmentNavigator(): Navigator { - return FragmentNavigator(requireContext(), childFragmentManager, id) - } - - // NavController doesn't set any pop animations when handling deep links - // Use this workaround to always set pop animations - @Navigator.Name("fragment") - class FragmentNavigator( - context: Context, - fragmentManager: FragmentManager, - @IdRes containerId: Int, - ) : androidx.navigation.fragment.FragmentNavigator(context, fragmentManager, containerId) { - override fun navigate( - entries: List, - navOptions: NavOptions?, - navigatorExtras: Navigator.Extras?, - ) = super.navigate(entries, navOptions?.overridePopAnimations(), navigatorExtras) - - override fun navigate( - destination: Destination, - args: Bundle?, - navOptions: NavOptions?, - navigatorExtras: Navigator.Extras?, - ) = super.navigate(destination, args, navOptions?.overridePopAnimations(), navigatorExtras) - - private fun NavOptions.overridePopAnimations() = - NavOptions.Builder() - .apply { - setPopEnterAnim(popEnterAnim.orDefault(R.animator.nav_default_pop_enter_anim)) - setPopExitAnim(popExitAnim.orDefault(R.animator.nav_default_pop_exit_anim)) - setEnterAnim(enterAnim) - setExitAnim(exitAnim) - setLaunchSingleTop(shouldLaunchSingleTop()) - setPopUpTo(popUpToId, isPopUpToInclusive()) - } - .build() - - private companion object { - fun Int.orDefault(@AnimatorRes defaultAnimator: Int): Int = - if (this != -1) this else defaultAnimator - } + model.getDeepLinkDestination(intent)?.let(deepLinkDestinations::trySend) } } diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/NavigationActivityViewModel.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/NavigationActivityViewModel.kt index 9d1c0072b..90f1d3c8d 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/NavigationActivityViewModel.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/NavigationActivityViewModel.kt @@ -5,112 +5,114 @@ package org.equeim.tremotesf.ui import android.app.Application -import android.content.ClipData -import android.content.ClipDescription import android.content.Intent -import android.os.Bundle -import androidx.annotation.IdRes +import android.net.Uri +import androidx.compose.ui.draganddrop.DragAndDropEvent +import androidx.compose.ui.draganddrop.mimeTypes +import androidx.compose.ui.draganddrop.toAndroidDragEvent import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.serialization.saved -import androidx.navigation.NavDeepLinkBuilder -import org.equeim.tremotesf.R import org.equeim.tremotesf.rpc.GlobalServers -import org.equeim.tremotesf.ui.addtorrent.AddTorrentFileFragmentArgs -import org.equeim.tremotesf.ui.addtorrent.AddTorrentLinkFragmentArgs +import org.equeim.tremotesf.ui.addtorrent.AddTorrentFileDestination +import org.equeim.tremotesf.ui.addtorrent.AddTorrentLinkDestination import org.equeim.tremotesf.ui.addtorrent.TORRENT_FILE_MIME_TYPE import org.equeim.tremotesf.ui.addtorrent.TORRENT_LINK_MIME_TYPES import org.equeim.tremotesf.ui.addtorrent.TorrentUri import org.equeim.tremotesf.ui.addtorrent.getTorrentUris -import org.equeim.tremotesf.ui.addtorrent.mimeTypes import org.equeim.tremotesf.ui.addtorrent.toTorrentUri +import org.equeim.tremotesf.ui.connectionsettings.ServerEditDestination +import org.equeim.tremotesf.ui.torrentproperties.TorrentPropertiesDestination +import org.equeim.tremotesf.ui.torrentslist.TorrentsListDestination import timber.log.Timber -class NavigationActivityViewModel(application: Application, savedStateHandle: SavedStateHandle) : - AndroidViewModel(application) { - var navigatedInitially: Boolean by savedStateHandle.saved { false } - - data class AddTorrentDirections(@param:IdRes val destinationId: Int, val arguments: Bundle) - - fun getAddTorrentDirections(intent: Intent): AddTorrentDirections? { - if (intent.action != Intent.ACTION_VIEW) return null - return intent.data - ?.toTorrentUri(getApplication(), validateUri = false) - ?.let { getAddTorrentDirections(listOf(it)) } - } - - fun getInitialDeepLinkIntent(intent: Intent): Intent? { - if ((intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) != 0) { - Timber.w("getInitialDeepLinkIntent: activity was launched from history, return null") - return null - } - - var deepLinkIntent: Intent? = getAddTorrentDirections(intent)?.run { - createDeepLinkIntent( - destinationId, - arguments, - intent - ) - } - if (deepLinkIntent == null) { - if ((intent.flags and Intent.FLAG_ACTIVITY_NEW_TASK) != 0) { - if (GlobalServers.serversState.value.servers.isEmpty()) { - deepLinkIntent = createDeepLinkIntent(R.id.server_edit_fragment, null, intent) +class NavigationActivityViewModel(application: Application) : AndroidViewModel(application) { + fun getInitialDestinations(intent: Intent, isTaskRoot: Boolean): List { + Timber.d("getInitialDestinations() called with: intent = $intent, isTaskRoot = $isTaskRoot") + if ((intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) == 0) { + val deepLinkDestination = getDeepLinkDestination(intent) + if (deepLinkDestination != null) { + return if (isTaskRoot) { + listOf(TorrentsListDestination, deepLinkDestination) + } else { + listOf(deepLinkDestination) } - } else { - Timber.i("getInitialDeepLinkIntent: we are not on our own task, return null") } } - return deepLinkIntent + return if (GlobalServers.serversState.value.servers.isEmpty()) { + listOf(TorrentsListDestination, ServerEditDestination()) + } else { + listOf(TorrentsListDestination) + } } - private fun createDeepLinkIntent( - @IdRes destinationId: Int, - arguments: Bundle?, - originalIntent: Intent, - ): Intent { - return NavDeepLinkBuilder(getApplication()) - .setGraph(R.navigation.nav_main) - .setDestination(destinationId) - .setArguments(arguments) - .createTaskStackBuilder() - .intents - .single() - .apply { - // Restore original intent's flags - flags = originalIntent.flags - // Prevent NavController from recreating activity if its intent doesn't have FLAG_ACTIVITY_CLEAR_TASK - if ((flags and Intent.FLAG_ACTIVITY_NEW_TASK) != 0 && (flags and Intent.FLAG_ACTIVITY_CLEAR_TASK) == 0) { - Timber.w("createDeepLinkIntent: add FLAG_ACTIVITY_CLEAR_TASK") - addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) - } + fun getDeepLinkDestination(intent: Intent): Destination? { + Timber.d("getDeepLinkDestination() called with: intent = $intent") + if (intent.action != Intent.ACTION_VIEW) { + Timber.d("getDeepLinkDestination: action is not VIEW") + return null + } + val uri = intent.data + if (uri == null) { + Timber.d("getDeepLinkDestination: data is null") + return null + } + Timber.d("getDeepLinkDestination: data = $uri") + return uri.parseAsInternalDeepLink() + ?: uri.toTorrentUri(getApplication(), validateUri = false)?.let { + getAddTorrentDestination(listOf(it)) } } - fun acceptDragStartEvent(clipDescription: ClipDescription): Boolean { - Timber.i("Drag start event mime types = ${clipDescription.mimeTypes()}") - return clipDescription.hasMimeType(TORRENT_FILE_MIME_TYPE) || - TORRENT_LINK_MIME_TYPES.any(clipDescription::hasMimeType) + fun shouldStartDragAndDrop(startEvent: DragAndDropEvent): Boolean { + val mimeTypes = startEvent.mimeTypes() + Timber.i("Received shouldStartDragAndDrop event, mime types = $mimeTypes") + val ok = mimeTypes.contains(TORRENT_FILE_MIME_TYPE) || TORRENT_LINK_MIME_TYPES.any(mimeTypes::contains) + if (ok) { + Timber.i("Accepting shouldStartDragAndDrop event") + } else { + Timber.i("Rejecting shouldStartDragAndDrop event") + } + return ok } - fun getAddTorrentDirections(clipData: ClipData): AddTorrentDirections? { - return getAddTorrentDirections(clipData.getTorrentUris(getApplication())) + fun getAddTorrentDestination(event: DragAndDropEvent): Destination? { + return getAddTorrentDestination(event.toAndroidDragEvent().clipData.getTorrentUris(getApplication())) } - private fun getAddTorrentDirections(uris: List): AddTorrentDirections? { + private fun getAddTorrentDestination(uris: List): Destination? { if (uris.isEmpty()) return null val firstUri = uris.first() return when (firstUri.type) { - TorrentUri.Type.File -> AddTorrentDirections( - R.id.add_torrent_file_fragment, - AddTorrentFileFragmentArgs(firstUri.uri).toBundle() - ) - TorrentUri.Type.Link -> AddTorrentDirections( - R.id.add_torrent_link_fragment, - AddTorrentLinkFragmentArgs( - uris.mapNotNull { it.takeIf { it.type == TorrentUri.Type.Link }?.uri }.toTypedArray() - ).toBundle() + TorrentUri.Type.File -> AddTorrentFileDestination(uri = firstUri.uri) + TorrentUri.Type.Link -> AddTorrentLinkDestination( + uris = uris.mapNotNull { it.takeIf { it.type == TorrentUri.Type.Link }?.uri } ) } } } + +fun Destination.toInternalDeepLink(): Uri { + val builder = Uri.Builder().scheme(INTERNAL_DEEP_LINK_SCHEME).authority(INTERNAL_DEEP_LINK_AUTHORITY) + when (this) { + is TorrentPropertiesDestination -> + builder.appendPath(TORRENT_PROPERTIES_PATH) + .appendPath(torrentHashString) + + else -> throw IllegalArgumentException("Destination $this can't be used in internal deep link") + } + return builder.build() +} + +fun Uri.parseAsInternalDeepLink(): Destination? { + if (scheme != INTERNAL_DEEP_LINK_SCHEME) return null + if (authority != INTERNAL_DEEP_LINK_AUTHORITY) return null + val pathSegments = this.pathSegments + if (pathSegments.getOrNull(0) != TORRENT_PROPERTIES_PATH) return null + val torrentHashString = pathSegments.getOrNull(1) ?: return null + return TorrentPropertiesDestination(torrentHashString).also { + Timber.d("Parsed $this as $it") + } +} + +private const val INTERNAL_DEEP_LINK_SCHEME = "tremotesf" +private const val INTERNAL_DEEP_LINK_AUTHORITY = "org.equeim.tremotesf" +private const val TORRENT_PROPERTIES_PATH = "torrentProperties" diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/Settings.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/Settings.kt index 830eb03f9..437959df9 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/Settings.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/Settings.kt @@ -21,7 +21,7 @@ import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import org.equeim.tremotesf.TremotesfApplication import org.equeim.tremotesf.rpc.requests.torrentproperties.TorrentLimits -import org.equeim.tremotesf.ui.torrentslistfragment.TorrentsListFragmentViewModel +import org.equeim.tremotesf.ui.torrentslist.TorrentsListScreenViewModel import timber.log.Timber import kotlin.reflect.KClass import kotlin.time.Duration @@ -245,31 +245,31 @@ object Settings { val askForMergingTrackersWhenAddingExistingTorrent: Property = PrefsProperty(key = "askForMergingTrackersWhenAddingExistingTorrent", defaultValue = true) - val torrentsSortMode: Property = + val torrentsSortMode: Property = PrefsProperty(key = "torrentsSortMode", defaultValue = -1).map( prefsToMapped = { - TorrentsListFragmentViewModel.SortMode.entries.getOrElse(it) { - TorrentsListFragmentViewModel.SortMode.DEFAULT + TorrentsListScreenViewModel.SortMode.entries.getOrElse(it) { + TorrentsListScreenViewModel.SortMode.DEFAULT } }, mappedToPrefs = { it.ordinal } ) - val torrentsSortOrder: Property = + val torrentsSortOrder: Property = PrefsProperty(key = "torrentsSortOrder", defaultValue = -1).map( prefsToMapped = { - TorrentsListFragmentViewModel.SortOrder.entries.getOrElse(it) { - TorrentsListFragmentViewModel.SortOrder.DEFAULT + TorrentsListScreenViewModel.SortOrder.entries.getOrElse(it) { + TorrentsListScreenViewModel.SortOrder.DEFAULT } }, mappedToPrefs = { it.ordinal } ) - val torrentsStatusFilter: Property = + val torrentsStatusFilter: Property = PrefsProperty(key = "torrentsStatusFilter", defaultValue = -1).map( prefsToMapped = { - TorrentsListFragmentViewModel.StatusFilterMode.entries.getOrElse(it) { - TorrentsListFragmentViewModel.StatusFilterMode.DEFAULT + TorrentsListScreenViewModel.StatusFilterMode.entries.getOrElse(it) { + TorrentsListScreenViewModel.StatusFilterMode.DEFAULT } }, mappedToPrefs = { it.ordinal } diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/SettingsFragment.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/SettingsScreen.kt similarity index 98% rename from app/src/main/kotlin/org/equeim/tremotesf/ui/SettingsFragment.kt rename to app/src/main/kotlin/org/equeim/tremotesf/ui/SettingsScreen.kt index 8ee4bd273..63ab74ab3 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/SettingsFragment.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/SettingsScreen.kt @@ -45,7 +45,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavController import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.channels.consumeEach @@ -55,6 +54,7 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.produceIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import kotlinx.parcelize.Parcelize import org.equeim.tremotesf.R import org.equeim.tremotesf.ui.Settings.BackgroundUpdateInterval import org.equeim.tremotesf.ui.Settings.ColorTheme @@ -68,12 +68,13 @@ import org.equeim.tremotesf.ui.components.TremotesfSwitchWithText import org.equeim.tremotesf.ui.components.TremotesfTopAppBar import org.equeim.tremotesf.ui.components.rememberTremotesfRuntimePermissionHelperState -class SettingsFragment : ComposeFragment() { +@Parcelize +data object SettingsDestination : Destination { @Composable override fun Content(navController: NavController) { val model = viewModel() - val properties: SettingsScreenViewModel.Properties? by model.properties.collectAsStateWithLifecycle() - properties?.let { SettingsScreen(navController::navigateUp, it) } + val properties by model.properties.collectAsStateWithLifecycle() + properties?.let { SettingsScreen(navController::popBackStack, it) } } } diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/TorrentsSetLocationDialogFragment.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/TorrentsSetLocationDialogScreen.kt similarity index 93% rename from app/src/main/kotlin/org/equeim/tremotesf/ui/TorrentsSetLocationDialogFragment.kt rename to app/src/main/kotlin/org/equeim/tremotesf/ui/TorrentsSetLocationDialogScreen.kt index 900316029..8e140e2e4 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/TorrentsSetLocationDialogFragment.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/TorrentsSetLocationDialogScreen.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider @@ -33,7 +34,9 @@ import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi import androidx.lifecycle.viewmodel.compose.saveable import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavController +import androidx.navigation3.runtime.metadata +import androidx.navigation3.scene.DialogSceneStrategy +import androidx.navigation3.scene.DialogSceneStrategy.Companion.DialogKey import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.StateFlow @@ -58,27 +61,34 @@ import org.equeim.tremotesf.ui.components.rememberTremotesfInitialFocusRequester import org.equeim.tremotesf.ui.components.updateAllDownloadDirectories import org.equeim.tremotesf.ui.utils.SnapshotStateListSaver import org.equeim.tremotesf.ui.utils.localeChangedEvents +import kotlinx.parcelize.Parcelize import java.util.Locale -class TorrentsSetLocationDialogFragment : ComposeDialogFragment() { +@Parcelize +data class TorrentsSetLocationDialogDestination( + val torrentHashStrings: List, + val location: String +) : Destination { @Composable override fun Content(navController: NavController) { - val args = TorrentsSetLocationDialogFragmentArgs.fromBundle(requireArguments()) val model = viewModel { TorrentSetLocationDialogViewModel( - torrentsHashStrings = args.torrentHashStrings.asList(), + torrentsHashStrings = torrentHashStrings, savedStateHandle = createSavedStateHandle(), application = checkNotNull(get(ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY)) ) } TorrentSetLocationDialogContent( allDownloadDirectoriesRequest = model.allDownloadDirectoriesRequest.collectAsStateWithLifecycle(), - initialLocation = args::location, + initialLocation = { location }, allDownloadDirectories = model.allDownloadDirectories, setLocation = model::setLocation, - onDismissRequest = ::dismiss + onDismissRequest = navController::popBackStack ) } + + override val metadata: Map + get() = metadata { put(DialogKey, DialogProperties()) } } @Composable diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/addtorrent/AddTorrentCommon.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/addtorrent/AddTorrentCommon.kt index bb280d626..2b9785e54 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/addtorrent/AddTorrentCommon.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/addtorrent/AddTorrentCommon.kt @@ -4,12 +4,15 @@ package org.equeim.tremotesf.ui.addtorrent +import android.app.Activity import android.content.Context +import android.content.Intent import android.content.res.Resources import android.os.Parcelable import android.view.View import android.widget.Toast import androidx.activity.ComponentActivity +import androidx.activity.compose.LocalActivity import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope @@ -41,12 +44,14 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.get -import androidx.navigation.NavController import kotlinx.parcelize.Parcelize import org.equeim.tremotesf.R import org.equeim.tremotesf.rpc.requests.torrentproperties.TorrentLimits import org.equeim.tremotesf.ui.Dimens +import org.equeim.tremotesf.ui.NavController +import org.equeim.tremotesf.ui.NavigationActivity import org.equeim.tremotesf.ui.ScreenPreview import org.equeim.tremotesf.ui.addtorrent.BaseAddTorrentModel.DownloadDirectoryFreeSpace import org.equeim.tremotesf.ui.components.DialogPadding @@ -57,7 +62,8 @@ import org.equeim.tremotesf.ui.components.TremotesfDownloadDirectoryField import org.equeim.tremotesf.ui.components.TremotesfLabelsEditor import org.equeim.tremotesf.ui.components.TremotesfSectionHeader import org.equeim.tremotesf.ui.components.TremotesfSwitchWithText -import org.equeim.tremotesf.ui.torrentslistfragment.TorrentsListFragmentViewModel +import org.equeim.tremotesf.ui.torrentslist.TorrentsListDestination +import org.equeim.tremotesf.ui.torrentslist.TorrentsListScreenViewModel import org.equeim.tremotesf.ui.utils.rememberFileSizeFormatter @Suppress("UnusedReceiverParameter") @@ -175,13 +181,18 @@ sealed interface AddTorrentState : Parcelable { fun HandleFinishedAddTorrentState( state: State, navController: NavController, - activity: ComponentActivity ) { val state = state.value if (state is AddTorrentState.Finished) { + val owner = navController.viewModelStoreOwnerForDestinationOrNull() + val activity = checkNotNull(LocalActivity.current) as ComponentActivity LaunchedEffect(null) { if (state.mergingTrackersMessage != null) { - showMergingTrackersMessageAfterAddingTorrent(navController, activity, state.mergingTrackersMessage) + showMergingTrackersMessageAfterAddingTorrent( + message = state.mergingTrackersMessage, + context = activity, + torrentsListScreenViewModelStoreOwner = owner + ) } activity.onBackPressedDispatcher.onBackPressed() } @@ -189,18 +200,14 @@ fun HandleFinishedAddTorrentState( } private fun showMergingTrackersMessageAfterAddingTorrent( - navController: NavController, + message: MergingTrackersMessage, context: Context, - message: MergingTrackersMessage + torrentsListScreenViewModelStoreOwner: ViewModelStoreOwner? ) { - val torrentsListScreenViewModel = try { - ViewModelProvider.create(navController.getBackStackEntry(R.id.torrents_list_fragment)) - .get() - } catch (_: IllegalArgumentException) { - null - } - if (torrentsListScreenViewModel != null) { - torrentsListScreenViewModel.showMergingTrackersMessage.value = message + if (torrentsListScreenViewModelStoreOwner != null) { + val viewModel = + ViewModelProvider.create(torrentsListScreenViewModelStoreOwner).get() + viewModel.showMergingTrackersMessage.value = message } else { Toast.makeText( context, @@ -366,3 +373,17 @@ private fun buildMultipleTorrentsListString(torrentNames: List, isRtl: B } } } + +fun NavController.popBackStackOrStartNewTask(activity: Activity) { + if (activity.isTaskRoot) { + popBackStack() + } else { + activity.startActivity( + Intent( + activity, + NavigationActivity::class.java + ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + ) + activity.finish() + } +} diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/addtorrent/AddTorrentFileFragment.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/addtorrent/AddTorrentFileScreen.kt similarity index 97% rename from app/src/main/kotlin/org/equeim/tremotesf/ui/addtorrent/AddTorrentFileFragment.kt rename to app/src/main/kotlin/org/equeim/tremotesf/ui/addtorrent/AddTorrentFileScreen.kt index b89c5e248..f7a266e49 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/addtorrent/AddTorrentFileFragment.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/addtorrent/AddTorrentFileScreen.kt @@ -5,6 +5,7 @@ package org.equeim.tremotesf.ui.addtorrent import android.Manifest +import androidx.activity.compose.LocalActivity import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -57,19 +58,19 @@ import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.createSavedStateHandle import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavController -import androidx.navigation.fragment.navArgs import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.launch +import kotlinx.parcelize.Parcelize import org.equeim.tremotesf.R import org.equeim.tremotesf.rpc.DetailedRpcRequestError import org.equeim.tremotesf.rpc.requests.FileSize import org.equeim.tremotesf.rpc.requests.torrentproperties.TorrentLimits import org.equeim.tremotesf.torrentfile.TorrentFilesTree -import org.equeim.tremotesf.ui.ComposeFragment +import org.equeim.tremotesf.ui.Destination import org.equeim.tremotesf.ui.DetailedConnectionErrorDialog import org.equeim.tremotesf.ui.Dimens +import org.equeim.tremotesf.ui.NavController import org.equeim.tremotesf.ui.ScreenPreview import org.equeim.tremotesf.ui.addtorrent.AddTorrentFileModel.LoadingState import org.equeim.tremotesf.ui.addtorrent.AddTorrentFileModel.LoadingState.FileLoadingError @@ -84,24 +85,21 @@ import org.equeim.tremotesf.ui.components.TremotesfTorrentsFilesList import org.equeim.tremotesf.ui.components.rememberTremotesfRuntimePermissionHelperState import org.equeim.tremotesf.ui.utils.rememberFileSizeFormatter - -class AddTorrentFileFragment : ComposeFragment() { - private val args: AddTorrentFileFragmentArgs by navArgs() - +@Parcelize +data class AddTorrentFileDestination(val uri: android.net.Uri) : Destination { @Composable override fun Content(navController: NavController) { - val model = viewModel { + val model = viewModel { AddTorrentFileModelImpl( - uri = args.uri, + uri = uri, application = checkNotNull(get(AndroidViewModelFactory.APPLICATION_KEY)), savedStateHandle = createSavedStateHandle() ) } + val activity = checkNotNull(LocalActivity.current) AddTorrentFileScreen( - navigateUp = navController::navigateUp, - + navigateUp = { navController.popBackStackOrStartNewTask(activity) }, needStoragePermission = model.needStoragePermission, - loadTorrentFile = model::load, loadingState = model.loadingState.collectAsStateWithLifecycle(), downloadDirectory = model.downloadDirectory, @@ -112,14 +110,15 @@ class AddTorrentFileFragment : ComposeFragment() { enabledLabels = model.enabledLabels, allLabels = model.allLabels, shouldShowLabels = model.shouldShowLabels.collectAsStateWithLifecycle(), - filesTree = model.filesTree, - addTorrent = model::addTorrentFile, addTorrentState = model.addTorrentState, onMergeTrackersDialogResult = model::onMergeTrackersDialogResult ) - HandleFinishedAddTorrentState(model.addTorrentState, navController, requireActivity()) + HandleFinishedAddTorrentState( + state = model.addTorrentState, + navController = navController + ) } } diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/addtorrent/AddTorrentLinkFragment.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/addtorrent/AddTorrentLinkScreen.kt similarity index 94% rename from app/src/main/kotlin/org/equeim/tremotesf/ui/addtorrent/AddTorrentLinkFragment.kt rename to app/src/main/kotlin/org/equeim/tremotesf/ui/addtorrent/AddTorrentLinkScreen.kt index 781496cf4..0fab21c50 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/addtorrent/AddTorrentLinkFragment.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/addtorrent/AddTorrentLinkScreen.kt @@ -4,6 +4,8 @@ package org.equeim.tremotesf.ui.addtorrent +import android.net.Uri +import androidx.activity.compose.LocalActivity import androidx.compose.foundation.draganddrop.dragAndDropTarget import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -52,14 +54,14 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.createSavedStateHandle import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavController -import androidx.navigation.fragment.navArgs +import kotlinx.parcelize.Parcelize import org.equeim.tremotesf.R import org.equeim.tremotesf.rpc.RpcRequestState import org.equeim.tremotesf.rpc.requests.FileSize import org.equeim.tremotesf.rpc.requests.torrentproperties.TorrentLimits -import org.equeim.tremotesf.ui.ComposeFragment +import org.equeim.tremotesf.ui.Destination import org.equeim.tremotesf.ui.Dimens +import org.equeim.tremotesf.ui.NavController import org.equeim.tremotesf.ui.ScreenPreview import org.equeim.tremotesf.ui.addtorrent.BaseAddTorrentModel.DownloadDirectoryFreeSpace import org.equeim.tremotesf.ui.components.DownloadDirectoryItem @@ -67,21 +69,22 @@ import org.equeim.tremotesf.ui.components.TremotesfScreenContentWithPlaceholder import org.equeim.tremotesf.ui.components.TremotesfTopAppBar import org.equeim.tremotesf.ui.components.rememberTremotesfInitialFocusRequester - -class AddTorrentLinkFragment : ComposeFragment() { - private val args: AddTorrentLinkFragmentArgs by navArgs() - +@Parcelize +data class AddTorrentLinkDestination( + val uris: List = emptyList() +) : Destination { @Composable override fun Content(navController: NavController) { val model = viewModel { AddTorrentLinkModel( - args.uris?.asList().orEmpty(), - createSavedStateHandle(), - checkNotNull(get(ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY)) + initialUris = uris, + savedStateHandle = createSavedStateHandle(), + application = checkNotNull(get(ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY)) ) } + val activity = checkNotNull(LocalActivity.current) AddTorrentLinkScreen( - navigateUp = navController::navigateUp, + navigateUp = { navController.popBackStackOrStartNewTask(activity) }, initialRpcInputsRequestState = model.initialRpcInputs.collectAsStateWithLifecycle(), torrentLinksText = model.torrentLinksText, downloadDirectory = model.downloadDirectory, @@ -99,7 +102,10 @@ class AddTorrentLinkFragment : ComposeFragment() { shouldStartDragAndDrop = model::shouldStartDragAndDrop, dragAndDropTarget = model ) - HandleFinishedAddTorrentState(model.addTorrentState, navController, requireActivity()) + HandleFinishedAddTorrentState( + state = model.addTorrentState, + navController = navController, + ) } } diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/connectionsettingsfragment/ConnectionSettingsFragment.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/connectionsettings/ConnectionSettingsScreen.kt similarity index 94% rename from app/src/main/kotlin/org/equeim/tremotesf/ui/connectionsettingsfragment/ConnectionSettingsFragment.kt rename to app/src/main/kotlin/org/equeim/tremotesf/ui/connectionsettings/ConnectionSettingsScreen.kt index cccbcc8bd..209f5d158 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/connectionsettingsfragment/ConnectionSettingsFragment.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/connectionsettings/ConnectionSettingsScreen.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -package org.equeim.tremotesf.ui.connectionsettingsfragment +package org.equeim.tremotesf.ui.connectionsettings import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues @@ -48,15 +48,16 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavController import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.parcelize.Parcelize import org.equeim.tremotesf.R import org.equeim.tremotesf.rpc.GlobalServers -import org.equeim.tremotesf.ui.ComposeFragment +import org.equeim.tremotesf.ui.Destination import org.equeim.tremotesf.ui.Dimens +import org.equeim.tremotesf.ui.NavController import org.equeim.tremotesf.ui.ScreenPreview import org.equeim.tremotesf.ui.components.TremotesfIconButtonWithTooltip import org.equeim.tremotesf.ui.components.TremotesfMultiSelectionPanel @@ -66,9 +67,9 @@ import org.equeim.tremotesf.ui.components.rememberTremotesfMultiSelectionState import org.equeim.tremotesf.ui.components.selectableBackground import org.equeim.tremotesf.ui.components.tremotesfMultiSelectionClickable import org.equeim.tremotesf.ui.utils.rememberAlphanumericComparator -import org.equeim.tremotesf.ui.utils.safeNavigate -class ConnectionSettingsFragment : ComposeFragment() { +@Parcelize +data object ConnectionSettingsDestination : Destination { @Composable override fun Content(navController: NavController) { val model = viewModel() @@ -78,13 +79,13 @@ class ConnectionSettingsFragment : ComposeFragment() { val sortedServers = remember { derivedStateOf { servers.value.sortedWith(comparator) } } ConnectionSettingsScreen( - navigateUp = navController::navigateUp, + navigateUp = navController::popBackStack, servers = sortedServers, currentServer = model.currentServer.collectAsStateWithLifecycle(), setCurrentServer = model::setCurrentServer, - editServer = { navController.safeNavigate(ConnectionSettingsFragmentDirections.toServerEditFragment(it)) }, + editServer = { navController.navigateTo(ServerEditDestination(it)) }, removeServers = model::removeServers, - addServer = { navController.safeNavigate(ConnectionSettingsFragmentDirections.toServerEditFragment()) } + addServer = { navController.navigateTo(ServerEditDestination()) } ) } } diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/connectionsettingsfragment/ServerEditFragment.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/connectionsettings/ServerEditScreen.kt similarity index 94% rename from app/src/main/kotlin/org/equeim/tremotesf/ui/connectionsettingsfragment/ServerEditFragment.kt rename to app/src/main/kotlin/org/equeim/tremotesf/ui/connectionsettings/ServerEditScreen.kt index 7311bc17b..6ab57c3ff 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/connectionsettingsfragment/ServerEditFragment.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/connectionsettings/ServerEditScreen.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -package org.equeim.tremotesf.ui.connectionsettingsfragment +package org.equeim.tremotesf.ui.connectionsettings import android.Manifest import android.app.Activity @@ -78,24 +78,23 @@ import androidx.lifecycle.compose.LifecycleEventEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.createSavedStateHandle import androidx.lifecycle.viewModelScope -import androidx.lifecycle.viewmodel.CreationExtras import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi import androidx.lifecycle.viewmodel.compose.saveable import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavController -import androidx.navigation.fragment.navArgs import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlinx.parcelize.Parcelize import org.equeim.tremotesf.BuildConfig import org.equeim.tremotesf.R import org.equeim.tremotesf.rpc.GlobalServers import org.equeim.tremotesf.rpc.Server -import org.equeim.tremotesf.ui.ComposeFragment +import org.equeim.tremotesf.ui.Destination import org.equeim.tremotesf.ui.Dimens import org.equeim.tremotesf.ui.Error +import org.equeim.tremotesf.ui.NavController import org.equeim.tremotesf.ui.ScreenPreview import org.equeim.tremotesf.ui.components.TremotesfComboBox import org.equeim.tremotesf.ui.components.TremotesfIntegerNumberInputFieldState @@ -107,26 +106,26 @@ import org.equeim.tremotesf.ui.components.UNSIGNED_16BIT_RANGE import org.equeim.tremotesf.ui.components.rememberTremotesfInitialFocusRequester import org.equeim.tremotesf.ui.components.rememberTremotesfIntegerNumberInputFieldState import org.equeim.tremotesf.ui.components.rememberTremotesfRuntimePermissionHelperState -import org.equeim.tremotesf.ui.utils.safeNavigate import timber.log.Timber import java.io.FileNotFoundException import java.net.Proxy import kotlin.time.Duration.Companion.seconds -class ServerEditFragment : ComposeFragment() { - private val args: ServerEditFragmentArgs by navArgs() - +@Parcelize +data class ServerEditDestination(val server: String? = null) : Destination { @Composable override fun Content(navController: NavController) { - val model = - viewModel( - viewModelStoreOwner = navController.getBackStackEntry(R.id.server_edit_fragment), - initializer = ServerEditFragmentViewModel.initializer(navController) + val model = viewModel { + ServerEditScreenViewModel( + originalServerName = server, + application = checkNotNull(get(ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY)), + savedStateHandle = createSavedStateHandle() ) + } LifecycleEventEffect(event = Lifecycle.Event.ON_START, onEvent = model::checkIfLocationEnabled) ServerEditScreen( - navigateUp = navController::navigateUp, - editingServer = args.server != null, + navigateUp = navController::popBackStack, + editingServer = server != null, name = model.name, address = model.address, port = model.port, @@ -141,8 +140,8 @@ class ServerEditFragment : ComposeFragment() { autoConnectOnWifiNetworkSSID = model.autoConnectOnWifiNetworkSSID, showSSIDErrorMessage = model.showSSIDErrorMessage, locationEnabled = model.locationEnabled.collectAsStateWithLifecycle(), - navigateToProxySettings = { navController.safeNavigate(ServerEditFragmentDirections.toProxySettingsFragment()) }, - navigateToCertificatesSettings = { navController.safeNavigate(ServerEditFragmentDirections.toCertificatesFragment()) }, + navigateToProxySettings = { navController.navigateTo(ServerProxySettingsDestination) }, + navigateToCertificatesSettings = { navController.navigateTo(ServerCertificatesDestination) }, setSSIDFromCurrentNetwork = model::setSSIDFromCurrentNetwork, isAboutToOverwriteServer = model::isAboutToOverwriteServer, saveServer = model::saveServer @@ -662,14 +661,14 @@ private fun ServerEditScreenPreview() = ScreenPreview { } @OptIn(SavedStateHandleSaveableApi::class) -class ServerEditFragmentViewModel( - private val args: ServerEditFragmentArgs, +class ServerEditScreenViewModel( + private val originalServerName: String?, application: Application, savedStateHandle: SavedStateHandle, ) : AndroidViewModel(application) { private val editingServer: Server = - if (args.server != null) { - GlobalServers.serversState.value.servers.find { it.name == args.server } ?: Server() + if (originalServerName != null) { + GlobalServers.serversState.value.servers.find { it.name == originalServerName } ?: Server() } else { Server() } @@ -798,36 +797,22 @@ class ServerEditFragmentViewModel( autoConnectOnWifiNetworkEnabled = autoConnectOnWifiNetworkEnabled.value, autoConnectOnWifiNetworkSSID = autoConnectOnWifiNetworkSSID.value.trim() ) - if (args.server == null || server != editingServer) { - GlobalServers.addOrReplaceServer(server, previousName = args.server) + if (originalServerName == null || server != editingServer) { + GlobalServers.addOrReplaceServer(server, previousName = originalServerName) } else { Timber.d("saveServer: server did not change") } } - - companion object { - fun initializer(navController: NavController): CreationExtras.() -> ServerEditFragmentViewModel { - return { - ServerEditFragmentViewModel( - args = ServerEditFragmentArgs.fromBundle(checkNotNull(navController.getBackStackEntry(R.id.server_edit_fragment).arguments)), - application = checkNotNull(get(ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY)), - savedStateHandle = createSavedStateHandle() - ) - } - } - } } -class ServerCertificatesFragment : ComposeFragment() { +@Parcelize +data object ServerCertificatesDestination : Destination { @Composable override fun Content(navController: NavController) { - val model = - viewModel( - viewModelStoreOwner = navController.getBackStackEntry(R.id.server_edit_fragment), - initializer = ServerEditFragmentViewModel.initializer(navController) - ) + val viewModelStoreOwner = navController.viewModelStoreOwnerForDestination() + val model = viewModel(viewModelStoreOwner = viewModelStoreOwner) ServerCertificatesScreen( - navigateUp = navController::navigateUp, + navigateUp = navController::popBackStack, selfSignedCertificateEnabled = model.selfSignedCertificateEnabled, selfSignedCertificate = model.selfSignedCertificate, clientCertificateEnabled = model.clientCertificateEnabled, @@ -849,7 +834,7 @@ private fun ServerCertificatesScreen( loadClientCertificateFromFile: (Uri) -> Unit, ) { Scaffold(topBar = { - TremotesfTopAppBar(stringResource(R.string.proxy_settings), navigateUp) + TremotesfTopAppBar(stringResource(R.string.certificates), navigateUp) }) { innerPadding -> Column( Modifier @@ -968,16 +953,14 @@ private object GetPemFileContract : ActivityResultContract() { } } -class ServerProxySettingsFragment : ComposeFragment() { +@Parcelize +data object ServerProxySettingsDestination : Destination { @Composable override fun Content(navController: NavController) { - val model = - viewModel( - viewModelStoreOwner = navController.getBackStackEntry(R.id.server_edit_fragment), - initializer = ServerEditFragmentViewModel.initializer(navController) - ) + val viewModelStoreOwner = navController.viewModelStoreOwnerForDestination() + val model = viewModel(viewModelStoreOwner = viewModelStoreOwner) ServerProxySettingsScreen( - navigateUp = navController::navigateUp, + navigateUp = navController::popBackStack, proxyType = model.proxyType, proxyHostname = model.proxyHostname, proxyPort = model.proxyPort, diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/serversettingsfragment/DownloadingFragment.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/serversettings/DownloadingScreen.kt similarity index 94% rename from app/src/main/kotlin/org/equeim/tremotesf/ui/serversettingsfragment/DownloadingFragment.kt rename to app/src/main/kotlin/org/equeim/tremotesf/ui/serversettings/DownloadingScreen.kt index 970406c22..305264384 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/serversettingsfragment/DownloadingFragment.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/serversettings/DownloadingScreen.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -package org.equeim.tremotesf.ui.serversettingsfragment +package org.equeim.tremotesf.ui.serversettings import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -21,10 +21,10 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavController import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.flow.StateFlow +import kotlinx.parcelize.Parcelize import org.equeim.tremotesf.R import org.equeim.tremotesf.rpc.GlobalRpcClient import org.equeim.tremotesf.rpc.RpcClient @@ -38,17 +38,19 @@ import org.equeim.tremotesf.rpc.requests.serversettings.setIncompleteDirectoryEn import org.equeim.tremotesf.rpc.requests.serversettings.setRenameIncompleteFiles import org.equeim.tremotesf.rpc.requests.serversettings.setStartAddedTorrents import org.equeim.tremotesf.rpc.toNativeSeparators -import org.equeim.tremotesf.ui.ComposeFragment +import org.equeim.tremotesf.ui.Destination +import org.equeim.tremotesf.ui.NavController import org.equeim.tremotesf.ui.ScreenPreview import org.equeim.tremotesf.ui.components.TremotesfSwitchWithText -class DownloadingFragment : ComposeFragment() { +@Parcelize +data object ServerSettingsDownloadingDestination : Destination { @Composable override fun Content(navController: NavController) { - val model = viewModel() + val model = viewModel() ServerSettingsDownloadingScreen( settingsRequestState = model.settings.collectAsStateWithLifecycle(), - navigateUp = navController::navigateUp, + navigateUp = navController::popBackStack, downloadDirectory = model.downloadDirectory, startAddedTorrents = model.startAddedTorrents, renameIncompleteTorrents = model.renameIncompleteFiles, @@ -59,7 +61,7 @@ class DownloadingFragment : ComposeFragment() { } } -class DownloadingFragmentViewModel : ViewModel() { +class DownloadingScreenViewModel : ViewModel() { val settings: StateFlow> = GlobalRpcClient.performRecoveringRequestIntoStateFlow(viewModelScope) { setInitialState(getDownloadingServerSettings()) diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/serversettingsfragment/NetworkFragment.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/serversettings/NetworkScreen.kt similarity index 96% rename from app/src/main/kotlin/org/equeim/tremotesf/ui/serversettingsfragment/NetworkFragment.kt rename to app/src/main/kotlin/org/equeim/tremotesf/ui/serversettings/NetworkScreen.kt index ec5341ed2..470cf82b4 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/serversettingsfragment/NetworkFragment.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/serversettings/NetworkScreen.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -package org.equeim.tremotesf.ui.serversettingsfragment +package org.equeim.tremotesf.ui.serversettings import android.app.Application import androidx.compose.foundation.layout.fillMaxWidth @@ -18,10 +18,10 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavController import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.flow.StateFlow +import kotlinx.parcelize.Parcelize import org.equeim.tremotesf.R import org.equeim.tremotesf.rpc.GlobalRpcClient import org.equeim.tremotesf.rpc.RpcClient @@ -40,7 +40,8 @@ import org.equeim.tremotesf.rpc.requests.serversettings.setUsePEX import org.equeim.tremotesf.rpc.requests.serversettings.setUsePortForwarding import org.equeim.tremotesf.rpc.requests.serversettings.setUseRandomPort import org.equeim.tremotesf.rpc.requests.serversettings.setUseUTP -import org.equeim.tremotesf.ui.ComposeFragment +import org.equeim.tremotesf.ui.Destination +import org.equeim.tremotesf.ui.NavController import org.equeim.tremotesf.ui.ScreenPreview import org.equeim.tremotesf.ui.components.TremotesfComboBox import org.equeim.tremotesf.ui.components.TremotesfIntegerNumberInputFieldState @@ -50,14 +51,14 @@ import org.equeim.tremotesf.ui.components.TremotesfSwitchWithText import org.equeim.tremotesf.ui.components.UNSIGNED_16BIT_RANGE import org.equeim.tremotesf.ui.components.rememberTremotesfIntegerNumberInputFieldState - -class NetworkFragment : ComposeFragment() { +@Parcelize +data object ServerSettingsNetworkDestination : Destination { @Composable override fun Content(navController: NavController) { - val model = viewModel() + val model = viewModel() ServerSettingsNetworkScreen( settingsRequestState = model.settings.collectAsStateWithLifecycle(), - navigateUp = navController::navigateUp, + navigateUp = navController::popBackStack, peerPort = model.peerPort, useRandomPort = model.useRandomPort, usePortForwarding = model.usePortForwarding, @@ -73,7 +74,6 @@ class NetworkFragment : ComposeFragment() { } } - @Composable private fun ServerSettingsNetworkScreen( settingsRequestState: State>, @@ -213,7 +213,7 @@ private fun ServerSettingsNetworkScreenPreview() = ScreenPreview { ) } -class NetworkFragmentViewModel(application: Application) : AndroidViewModel(application) { +class NetworkScreenViewModel(application: Application) : AndroidViewModel(application) { val settings: StateFlow> = GlobalRpcClient.performRecoveringRequestIntoStateFlow(viewModelScope) { setInitialState(getNetworkServerSettings()) } diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/serversettingsfragment/QueueFragment.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/serversettings/QueueScreen.kt similarity index 94% rename from app/src/main/kotlin/org/equeim/tremotesf/ui/serversettingsfragment/QueueFragment.kt rename to app/src/main/kotlin/org/equeim/tremotesf/ui/serversettings/QueueScreen.kt index f07566f88..32da1cc4d 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/serversettingsfragment/QueueFragment.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/serversettings/QueueScreen.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -package org.equeim.tremotesf.ui.serversettingsfragment +package org.equeim.tremotesf.ui.serversettings import android.app.Application import androidx.compose.foundation.layout.fillMaxWidth @@ -17,10 +17,10 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavController import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.flow.StateFlow +import kotlinx.parcelize.Parcelize import org.equeim.tremotesf.R import org.equeim.tremotesf.rpc.GlobalRpcClient import org.equeim.tremotesf.rpc.RpcClient @@ -34,7 +34,8 @@ import org.equeim.tremotesf.rpc.requests.serversettings.setIgnoreQueueIfIdle import org.equeim.tremotesf.rpc.requests.serversettings.setIgnoreQueueIfIdleFor import org.equeim.tremotesf.rpc.requests.serversettings.setSeedQueueEnabled import org.equeim.tremotesf.rpc.requests.serversettings.setSeedQueueSize -import org.equeim.tremotesf.ui.ComposeFragment +import org.equeim.tremotesf.ui.Destination +import org.equeim.tremotesf.ui.NavController import org.equeim.tremotesf.ui.ScreenPreview import org.equeim.tremotesf.ui.components.NON_NEGATIVE_INTEGERS_RANGE import org.equeim.tremotesf.ui.components.TremotesfIntegerNumberInputFieldState @@ -44,13 +45,14 @@ import org.equeim.tremotesf.ui.components.UNSIGNED_16BIT_RANGE import org.equeim.tremotesf.ui.components.rememberTremotesfIntegerNumberInputFieldState import kotlin.time.Duration.Companion.minutes -class QueueFragment : ComposeFragment() { +@Parcelize +data object ServerSettingsQueueDestination : Destination { @Composable override fun Content(navController: NavController) { - val model = viewModel() + val model = viewModel() ServerSettingsQueueScreen( settingsRequestState = model.settings.collectAsStateWithLifecycle(), - navigateUp = navController::navigateUp, + navigateUp = navController::popBackStack, downloadQueueEnabled = model.downloadQueueEnabled, downloadQueueSize = model.downloadQueueSize, seedQueueEnabled = model.seedQueueEnabled, @@ -62,7 +64,7 @@ class QueueFragment : ComposeFragment() { } } -class QueueFragmentViewModel(application: Application) : AndroidViewModel(application) { +class QueueScreenViewModel(application: Application) : AndroidViewModel(application) { val settings: StateFlow> = GlobalRpcClient.performRecoveringRequestIntoStateFlow(viewModelScope) { setInitialState(getQueueServerSettings()) } diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/serversettingsfragment/SeedingFragment.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/serversettings/SeedingScreen.kt similarity index 93% rename from app/src/main/kotlin/org/equeim/tremotesf/ui/serversettingsfragment/SeedingFragment.kt rename to app/src/main/kotlin/org/equeim/tremotesf/ui/serversettings/SeedingScreen.kt index 85f1f7bc1..8e204cf63 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/serversettingsfragment/SeedingFragment.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/serversettings/SeedingScreen.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -package org.equeim.tremotesf.ui.serversettingsfragment +package org.equeim.tremotesf.ui.serversettings import android.app.Application import androidx.compose.foundation.layout.fillMaxWidth @@ -17,10 +17,10 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavController import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.flow.StateFlow +import kotlinx.parcelize.Parcelize import org.equeim.tremotesf.R import org.equeim.tremotesf.rpc.GlobalRpcClient import org.equeim.tremotesf.rpc.RpcClient @@ -32,7 +32,8 @@ import org.equeim.tremotesf.rpc.requests.serversettings.setServerIdleSeedingLimi import org.equeim.tremotesf.rpc.requests.serversettings.setServerIdleSeedingLimited import org.equeim.tremotesf.rpc.requests.serversettings.setServerRatioLimit import org.equeim.tremotesf.rpc.requests.serversettings.setServerRatioLimited -import org.equeim.tremotesf.ui.ComposeFragment +import org.equeim.tremotesf.ui.Destination +import org.equeim.tremotesf.ui.NavController import org.equeim.tremotesf.ui.ScreenPreview import org.equeim.tremotesf.ui.components.NON_NEGATIVE_DECIMALS_RANGE import org.equeim.tremotesf.ui.components.TremotesfDecimalNumberInputFieldState @@ -44,13 +45,14 @@ import org.equeim.tremotesf.ui.components.rememberTremotesfDecimalNumberInputFie import org.equeim.tremotesf.ui.components.rememberTremotesfIntegerNumberInputFieldState import kotlin.time.Duration.Companion.minutes -class SeedingFragment : ComposeFragment() { +@Parcelize +data object ServerSettingsSeedingDestination : Destination { @Composable override fun Content(navController: NavController) { - val model = viewModel() + val model = viewModel() ServerSettingsSeedingScreen( settingsRequestState = model.settings.collectAsStateWithLifecycle(), - navigateUp = navController::navigateUp, + navigateUp = navController::popBackStack, ratioLimited = model.ratioLimited, ratioLimit = model.ratioLimit, idleSeedingLimited = model.idleSeedingLimited, @@ -58,10 +60,9 @@ class SeedingFragment : ComposeFragment() { backgroundRpcRequestsErrors = GlobalRpcClient.backgroundRpcRequestsErrors ) } - } -class SeedingFragmentViewModel(application: Application) : AndroidViewModel(application) { +class SeedingScreenViewModel(application: Application) : AndroidViewModel(application) { val settings: StateFlow> = GlobalRpcClient.performRecoveringRequestIntoStateFlow(viewModelScope) { setInitialState(getSeedingServerSettings()) } diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/serversettingsfragment/ServerSettingsCategory.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/serversettings/ServerSettingsCategory.kt similarity index 98% rename from app/src/main/kotlin/org/equeim/tremotesf/ui/serversettingsfragment/ServerSettingsCategory.kt rename to app/src/main/kotlin/org/equeim/tremotesf/ui/serversettings/ServerSettingsCategory.kt index 2ec2a60e5..d1c55b486 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/serversettingsfragment/ServerSettingsCategory.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/serversettings/ServerSettingsCategory.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -package org.equeim.tremotesf.ui.serversettingsfragment +package org.equeim.tremotesf.ui.serversettings import androidx.annotation.StringRes import androidx.compose.foundation.layout.Arrangement diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/serversettingsfragment/ServerSettingsFragment.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/serversettings/ServerSettingsScreen.kt similarity index 64% rename from app/src/main/kotlin/org/equeim/tremotesf/ui/serversettingsfragment/ServerSettingsFragment.kt rename to app/src/main/kotlin/org/equeim/tremotesf/ui/serversettings/ServerSettingsScreen.kt index 1016d51ac..1427cebe1 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/serversettingsfragment/ServerSettingsFragment.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/serversettings/ServerSettingsScreen.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -package org.equeim.tremotesf.ui.serversettingsfragment +package org.equeim.tremotesf.ui.serversettings import androidx.annotation.StringRes import androidx.compose.foundation.clickable @@ -19,26 +19,26 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.navigation.NavController -import androidx.navigation.NavDirections +import kotlinx.parcelize.Parcelize import org.equeim.tremotesf.R -import org.equeim.tremotesf.ui.ComposeFragment +import org.equeim.tremotesf.ui.Destination +import org.equeim.tremotesf.ui.NavController import org.equeim.tremotesf.ui.ScreenPreview import org.equeim.tremotesf.ui.components.TremotesfTopAppBar -import org.equeim.tremotesf.ui.utils.safeNavigate -class ServerSettingsFragment : ComposeFragment() { +@Parcelize +data object ServerSettingsDestination : Destination { @Composable override fun Content(navController: NavController) { ServerSettingsScreen( - navigateUp = navController::navigateUp, - navigate = navController::safeNavigate, + navigateUp = navController::popBackStack, + navigateTo = navController::navigateTo ) } } @Composable -private fun ServerSettingsScreen(navigateUp: () -> Unit, navigate: (NavDirections) -> Unit) { +private fun ServerSettingsScreen(navigateUp: () -> Unit, navigateTo: (Destination) -> Unit) { Scaffold( topBar = { TremotesfTopAppBar( @@ -54,21 +54,11 @@ private fun ServerSettingsScreen(navigateUp: () -> Unit, navigate: (NavDirection .consumeWindowInsets(innerPadding) .padding(innerPadding) ) { - PageListItem(R.string.server_settings_downloading) { - navigate(ServerSettingsFragmentDirections.toDownloadingFragment()) - } - PageListItem(R.string.server_settings_seeding) { - navigate(ServerSettingsFragmentDirections.toSeedingFragment()) - } - PageListItem(R.string.server_settings_queue) { - navigate(ServerSettingsFragmentDirections.toQueueFragment()) - } - PageListItem(R.string.server_settings_speed) { - navigate(ServerSettingsFragmentDirections.toSpeedFragment()) - } - PageListItem(R.string.server_settings_network) { - navigate(ServerSettingsFragmentDirections.toNetworkFragment()) - } + PageListItem(R.string.server_settings_downloading) { navigateTo(ServerSettingsDownloadingDestination) } + PageListItem(R.string.server_settings_seeding) { navigateTo(ServerSettingsSeedingDestination) } + PageListItem(R.string.server_settings_queue) { navigateTo(ServerSettingsQueueDestination) } + PageListItem(R.string.server_settings_speed) { navigateTo(ServerSettingsSpeedDestination) } + PageListItem(R.string.server_settings_network) { navigateTo(ServerSettingsNetworkDestination) } } } } @@ -88,6 +78,6 @@ private fun PageListItem(@StringRes title: Int, onClick: () -> Unit) { private fun ServerSettingsScreenPreview() = ScreenPreview { ServerSettingsScreen( navigateUp = {}, - navigate = {} + navigateTo = {} ) } diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/serversettingsfragment/SpeedFragment.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/serversettings/SpeedScreen.kt similarity index 97% rename from app/src/main/kotlin/org/equeim/tremotesf/ui/serversettingsfragment/SpeedFragment.kt rename to app/src/main/kotlin/org/equeim/tremotesf/ui/serversettings/SpeedScreen.kt index ea3b3a097..55d91639c 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/serversettingsfragment/SpeedFragment.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/serversettings/SpeedScreen.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -package org.equeim.tremotesf.ui.serversettingsfragment +package org.equeim.tremotesf.ui.serversettings import android.annotation.SuppressLint import android.app.Application @@ -39,10 +39,10 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavController import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.flow.StateFlow +import kotlinx.parcelize.Parcelize import org.equeim.tremotesf.R import org.equeim.tremotesf.rpc.GlobalRpcClient import org.equeim.tremotesf.rpc.RpcClient @@ -64,8 +64,9 @@ import org.equeim.tremotesf.rpc.requests.serversettings.setDownloadSpeedLimited import org.equeim.tremotesf.rpc.requests.serversettings.setUploadSpeedLimit import org.equeim.tremotesf.rpc.requests.serversettings.setUploadSpeedLimited import org.equeim.tremotesf.ui.ComponentPreview -import org.equeim.tremotesf.ui.ComposeFragment +import org.equeim.tremotesf.ui.Destination import org.equeim.tremotesf.ui.Dimens +import org.equeim.tremotesf.ui.NavController import org.equeim.tremotesf.ui.ScreenPreview import org.equeim.tremotesf.ui.applyDisabledAlpha import org.equeim.tremotesf.ui.components.TremotesfComboBox @@ -82,15 +83,15 @@ import java.time.format.DateTimeFormatter import java.time.format.FormatStyle import java.time.format.TextStyle import java.time.temporal.WeekFields -import java.util.Locale -class SpeedFragment : ComposeFragment() { +@Parcelize +data object ServerSettingsSpeedDestination : Destination { @Composable override fun Content(navController: NavController) { - val model = viewModel() + val model = viewModel() ServerSettingsSpeedScreen( settingsRequestState = model.settings.collectAsStateWithLifecycle(), - navigateUp = navController::navigateUp, + navigateUp = navController::popBackStack, downloadSpeedLimited = model.downloadSpeedLimited, downloadSpeedLimit = model.downloadSpeedLimit, uploadSpeedLimited = model.uploadSpeedLimited, @@ -371,7 +372,7 @@ private fun LimitsScheduleTimePreview() = ComponentPreview { LimitsScheduleTime(label = R.string.from, time = remember { ServerSettingsProperty(LocalTime.NOON) {} }) } -class SpeedFragmentViewModel(application: Application) : AndroidViewModel(application) { +class SpeedScreenViewModel(application: Application) : AndroidViewModel(application) { val settings: StateFlow> = GlobalRpcClient.performRecoveringRequestIntoStateFlow(viewModelScope) { setInitialState(getSpeedServerSettings()) } diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentpropertiesfragment/DetailsTab.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentproperties/DetailsTab.kt similarity index 99% rename from app/src/main/kotlin/org/equeim/tremotesf/ui/torrentpropertiesfragment/DetailsTab.kt rename to app/src/main/kotlin/org/equeim/tremotesf/ui/torrentproperties/DetailsTab.kt index 59bd2b418..7546c19f1 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentpropertiesfragment/DetailsTab.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentproperties/DetailsTab.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -package org.equeim.tremotesf.ui.torrentpropertiesfragment +package org.equeim.tremotesf.ui.torrentproperties import android.text.format.DateUtils import androidx.compose.foundation.layout.Arrangement diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentpropertiesfragment/FilesTab.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentproperties/FilesTab.kt similarity index 92% rename from app/src/main/kotlin/org/equeim/tremotesf/ui/torrentpropertiesfragment/FilesTab.kt rename to app/src/main/kotlin/org/equeim/tremotesf/ui/torrentproperties/FilesTab.kt index f1727e7d3..6c30489f3 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentpropertiesfragment/FilesTab.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentproperties/FilesTab.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -package org.equeim.tremotesf.ui.torrentpropertiesfragment +package org.equeim.tremotesf.ui.torrentproperties import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -44,7 +44,7 @@ import java.text.DecimalFormat fun FilesTab( innerPadding: PaddingValues, filesTree: TorrentFilesTree, - filesTreeState: StateFlow, + filesTreeState: StateFlow, toolbarClicked: Flow, ) { val filesTreeState = filesTreeState.collectAsStateWithLifecycle() @@ -55,7 +55,7 @@ fun FilesTab( } when (val state = filesTreeState.value) { - is TorrentPropertiesFragmentViewModel.FilesTreeState.Loading -> TremotesfLoadingPlaceholder( + is TorrentPropertiesScreenViewModel.FilesTreeState.Loading -> TremotesfLoadingPlaceholder( modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()) @@ -63,7 +63,7 @@ fun FilesTab( .padding(Dimens.screenContentPadding()) ) - is TorrentPropertiesFragmentViewModel.FilesTreeState.Error -> TremotesfErrorPlaceholder( + is TorrentPropertiesScreenViewModel.FilesTreeState.Error -> TremotesfErrorPlaceholder( error = state.error, onShowDetailedErrorButtonClicked = { showDetailedErrorDialog = it }, modifier = Modifier @@ -73,7 +73,7 @@ fun FilesTab( .padding(Dimens.screenContentPadding()) ) - is TorrentPropertiesFragmentViewModel.FilesTreeState.Loaded -> { + is TorrentPropertiesScreenViewModel.FilesTreeState.Loaded -> { if (!state.torrentHasFiles) { TremotesfErrorPlaceholder( error = stringResource(R.string.no_files), diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentpropertiesfragment/LimitsTab.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentproperties/LimitsTab.kt similarity index 99% rename from app/src/main/kotlin/org/equeim/tremotesf/ui/torrentpropertiesfragment/LimitsTab.kt rename to app/src/main/kotlin/org/equeim/tremotesf/ui/torrentproperties/LimitsTab.kt index 0126dde7f..ac7a63a57 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentpropertiesfragment/LimitsTab.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentproperties/LimitsTab.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -package org.equeim.tremotesf.ui.torrentpropertiesfragment +package org.equeim.tremotesf.ui.torrentproperties import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentpropertiesfragment/PeersTab.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentproperties/PeersTab.kt similarity index 99% rename from app/src/main/kotlin/org/equeim/tremotesf/ui/torrentpropertiesfragment/PeersTab.kt rename to app/src/main/kotlin/org/equeim/tremotesf/ui/torrentproperties/PeersTab.kt index fb9ee217a..7683167e6 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentpropertiesfragment/PeersTab.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentproperties/PeersTab.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -package org.equeim.tremotesf.ui.torrentpropertiesfragment +package org.equeim.tremotesf.ui.torrentproperties import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentpropertiesfragment/RpcTorrentFilesTree.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentproperties/RpcTorrentFilesTree.kt similarity index 99% rename from app/src/main/kotlin/org/equeim/tremotesf/ui/torrentpropertiesfragment/RpcTorrentFilesTree.kt rename to app/src/main/kotlin/org/equeim/tremotesf/ui/torrentproperties/RpcTorrentFilesTree.kt index 7bf5e2c38..aba296c3a 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentpropertiesfragment/RpcTorrentFilesTree.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentproperties/RpcTorrentFilesTree.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -package org.equeim.tremotesf.ui.torrentpropertiesfragment +package org.equeim.tremotesf.ui.torrentproperties import androidx.annotation.MainThread import androidx.lifecycle.SavedStateHandle diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentpropertiesfragment/TorrentPropertiesFragment.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentproperties/TorrentPropertiesScreen.kt similarity index 93% rename from app/src/main/kotlin/org/equeim/tremotesf/ui/torrentpropertiesfragment/TorrentPropertiesFragment.kt rename to app/src/main/kotlin/org/equeim/tremotesf/ui/torrentproperties/TorrentPropertiesScreen.kt index 6137ae6e6..a8302e30f 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentpropertiesfragment/TorrentPropertiesFragment.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentproperties/TorrentPropertiesScreen.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -package org.equeim.tremotesf.ui.torrentpropertiesfragment +package org.equeim.tremotesf.ui.torrentproperties import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column @@ -44,12 +44,12 @@ import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.AP import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.createSavedStateHandle import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavController import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch +import kotlinx.parcelize.Parcelize import org.equeim.tremotesf.R import org.equeim.tremotesf.rpc.GlobalRpcClient import org.equeim.tremotesf.rpc.RpcRequestState @@ -59,49 +59,50 @@ import org.equeim.tremotesf.rpc.requests.torrentproperties.TorrentDetails import org.equeim.tremotesf.rpc.requests.torrentproperties.TorrentLimits import org.equeim.tremotesf.rpc.toNativeSeparators import org.equeim.tremotesf.torrentfile.TorrentFilesTree -import org.equeim.tremotesf.ui.ComposeFragment +import org.equeim.tremotesf.ui.Destination import org.equeim.tremotesf.ui.Dimens +import org.equeim.tremotesf.ui.LabelsEditDialogDestination +import org.equeim.tremotesf.ui.NavController import org.equeim.tremotesf.ui.Pause import org.equeim.tremotesf.ui.ShowRpcErrorsSnackbar import org.equeim.tremotesf.ui.TorrentRenameDialog import org.equeim.tremotesf.ui.TorrentsRemoveDialog +import org.equeim.tremotesf.ui.TorrentsSetLocationDialogDestination import org.equeim.tremotesf.ui.components.TremotesfIconButtonWithTooltip import org.equeim.tremotesf.ui.components.TremotesfIconButtonWithTooltipAndMenu import org.equeim.tremotesf.ui.components.TremotesfScreenContentWithPlaceholder import org.equeim.tremotesf.ui.components.TremotesfScrollableTopAppBar import org.equeim.tremotesf.ui.components.TremotesfTopAppBarDefaults import org.equeim.tremotesf.ui.utils.Utils -import org.equeim.tremotesf.ui.utils.safeNavigate - -class TorrentPropertiesFragment : ComposeFragment() { +@Parcelize +data class TorrentPropertiesDestination(val torrentHashString: String) : Destination { @Composable override fun Content(navController: NavController) { - val args = TorrentPropertiesFragmentArgs.fromBundle(requireArguments()) val model = viewModel { - TorrentPropertiesFragmentViewModel( - torrentHashString = args.torrentHashString, + TorrentPropertiesScreenViewModel( + torrentHashString = torrentHashString, application = checkNotNull(get(APPLICATION_KEY)), savedStateHandle = createSavedStateHandle() ) } if (model.shouldNavigateUp) { - LaunchedEffect(null) { navController.navigateUp() } + LaunchedEffect(null) { navController.popBackStack() } } TorrentPropertiesScreen( - navigateUp = navController::navigateUp, + navigateUp = navController::popBackStack, navigateToLabelsEditDialog = { enabledLabels -> - navController.safeNavigate( - TorrentPropertiesFragmentDirections.toLabelsEditDialog( - torrentHashStrings = arrayOf(args.torrentHashString), - enabledLabels = enabledLabels.toTypedArray() + navController.navigateTo( + LabelsEditDialogDestination( + torrentHashStrings = listOf(torrentHashString), + enabledLabels = enabledLabels ) ) }, navigateToSetLocationDialog = { location -> - navController.safeNavigate( - TorrentPropertiesFragmentDirections.toTorrentsSetLocationDialog( - torrentHashStrings = arrayOf(args.torrentHashString), + navController.navigateTo( + TorrentsSetLocationDialogDestination( + torrentHashStrings = listOf(torrentHashString), location = location ) ) @@ -141,9 +142,9 @@ private fun TorrentPropertiesScreen( torrentOperations: TorrentOperations, filesTree: TorrentFilesTree, - filesTreeState: StateFlow, + filesTreeState: StateFlow, - trackers: StateFlow>>, + trackers: StateFlow>>, peers: StateFlow>>, webSeeders: StateFlow>>, limits: StateFlow>, diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentpropertiesfragment/TorrentPropertiesFragmentViewModel.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentproperties/TorrentPropertiesScreenViewModel.kt similarity index 99% rename from app/src/main/kotlin/org/equeim/tremotesf/ui/torrentpropertiesfragment/TorrentPropertiesFragmentViewModel.kt rename to app/src/main/kotlin/org/equeim/tremotesf/ui/torrentproperties/TorrentPropertiesScreenViewModel.kt index 0bd86a05d..4a270e43d 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentpropertiesfragment/TorrentPropertiesFragmentViewModel.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentproperties/TorrentPropertiesScreenViewModel.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -package org.equeim.tremotesf.ui.torrentpropertiesfragment +package org.equeim.tremotesf.ui.torrentproperties import android.app.Application import androidx.annotation.StringRes @@ -77,7 +77,7 @@ import java.time.Instant import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds -class TorrentPropertiesFragmentViewModel( +class TorrentPropertiesScreenViewModel( val torrentHashString: String, application: Application, private val savedStateHandle: SavedStateHandle, diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentpropertiesfragment/TrackersTab.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentproperties/TrackersTab.kt similarity index 99% rename from app/src/main/kotlin/org/equeim/tremotesf/ui/torrentpropertiesfragment/TrackersTab.kt rename to app/src/main/kotlin/org/equeim/tremotesf/ui/torrentproperties/TrackersTab.kt index 07d9bea45..c3776e4e2 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentpropertiesfragment/TrackersTab.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentproperties/TrackersTab.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -package org.equeim.tremotesf.ui.torrentpropertiesfragment +package org.equeim.tremotesf.ui.torrentproperties import android.os.Parcelable import android.text.format.DateUtils @@ -76,7 +76,7 @@ import org.equeim.tremotesf.ui.components.rememberTremotesfInitialFocusRequester import org.equeim.tremotesf.ui.components.rememberTremotesfMultiSelectionState import org.equeim.tremotesf.ui.components.selectableBackground import org.equeim.tremotesf.ui.components.tremotesfMultiSelectionClickable -import org.equeim.tremotesf.ui.torrentpropertiesfragment.TorrentPropertiesFragmentViewModel.TrackerItem +import org.equeim.tremotesf.ui.torrentproperties.TorrentPropertiesScreenViewModel.TrackerItem import org.equeim.tremotesf.ui.utils.rememberLocaleDependentValue import java.time.Duration import java.time.Instant diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentpropertiesfragment/WebSeedersTab.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentproperties/WebSeedersTab.kt similarity index 98% rename from app/src/main/kotlin/org/equeim/tremotesf/ui/torrentpropertiesfragment/WebSeedersTab.kt rename to app/src/main/kotlin/org/equeim/tremotesf/ui/torrentproperties/WebSeedersTab.kt index 1032b23db..eb915c9a7 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentpropertiesfragment/WebSeedersTab.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentproperties/WebSeedersTab.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -package org.equeim.tremotesf.ui.torrentpropertiesfragment +package org.equeim.tremotesf.ui.torrentproperties import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/BottomBar.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslist/BottomBar.kt similarity index 91% rename from app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/BottomBar.kt rename to app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslist/BottomBar.kt index 7ee171828..ae8ed2a4f 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/BottomBar.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslist/BottomBar.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -package org.equeim.tremotesf.ui.torrentslistfragment +package org.equeim.tremotesf.ui.torrentslist import android.content.ActivityNotFoundException import android.net.Uri @@ -72,7 +72,7 @@ import org.equeim.tremotesf.ui.addtorrent.TORRENT_FILE_MIME_TYPE import org.equeim.tremotesf.ui.components.TremotesfFloatingActionButtonWithTooltip import org.equeim.tremotesf.ui.components.TremotesfIconButtonWithTooltip import org.equeim.tremotesf.ui.components.rememberTremotesfInitialFocusRequester -import org.equeim.tremotesf.ui.torrentslistfragment.TorrentsListFragmentViewModel.SortAndFilterSettings +import org.equeim.tremotesf.ui.torrentslist.TorrentsListScreenViewModel.SortAndFilterSettings import timber.log.Timber @OptIn(ExperimentalMaterial3Api::class) @@ -92,7 +92,7 @@ fun BottomBar( labelsEnabled: State, showTransmissionSettingsButton: State, showFiltersAndSearchButtons: State, - floatingActionButtonState: State, + floatingActionButtonState: State, navigateToServerAddingScreen: () -> Unit, navigateToAddTorrentFileScreen: (Uri) -> Unit, @@ -223,7 +223,7 @@ private fun Buttons( showTransmissionSettingsButton: State, showFiltersAndSearchButtons: State, sortAndFilterSettings: State, - floatingActionButtonState: State, + floatingActionButtonState: State, setShouldConnectToServer: (Boolean) -> Unit, showTransmissionSettingsBottomSheet: () -> Unit, showFiltersBottomSheet: () -> Unit, @@ -279,7 +279,7 @@ private fun Buttons( contentAlignment = Alignment.Center, ) { val fabState = floatingActionButtonState.value - if (fabState == TorrentsListFragmentViewModel.FloatingActionButtonState.AddTorrent) { + if (fabState == TorrentsListScreenViewModel.FloatingActionButtonState.AddTorrent) { TremotesfFloatingActionButtonWithTooltip( icon = Icons.Filled.Add, textId = R.string.add_torrent, @@ -292,17 +292,17 @@ private fun Buttons( elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation(), onClick = { when (fabState) { - TorrentsListFragmentViewModel.FloatingActionButtonState.Connect -> + TorrentsListScreenViewModel.FloatingActionButtonState.Connect -> setShouldConnectToServer(true) - TorrentsListFragmentViewModel.FloatingActionButtonState.Disconnect -> + TorrentsListScreenViewModel.FloatingActionButtonState.Disconnect -> setShouldConnectToServer(false) - TorrentsListFragmentViewModel.FloatingActionButtonState.AddServer -> + TorrentsListScreenViewModel.FloatingActionButtonState.AddServer -> navigateToServerAddingScreen() // Impossible case - TorrentsListFragmentViewModel.FloatingActionButtonState.AddTorrent -> + TorrentsListScreenViewModel.FloatingActionButtonState.AddTorrent -> throw IllegalStateException() } } @@ -310,11 +310,11 @@ private fun Buttons( Text( stringResource( when (fabState) { - TorrentsListFragmentViewModel.FloatingActionButtonState.Connect -> R.string.connect - TorrentsListFragmentViewModel.FloatingActionButtonState.Disconnect -> R.string.disconnect - TorrentsListFragmentViewModel.FloatingActionButtonState.AddServer -> R.string.add_server + TorrentsListScreenViewModel.FloatingActionButtonState.Connect -> R.string.connect + TorrentsListScreenViewModel.FloatingActionButtonState.Disconnect -> R.string.disconnect + TorrentsListScreenViewModel.FloatingActionButtonState.AddServer -> R.string.add_server // Impossible case - TorrentsListFragmentViewModel.FloatingActionButtonState.AddTorrent -> throw IllegalStateException() + TorrentsListScreenViewModel.FloatingActionButtonState.AddTorrent -> throw IllegalStateException() } ) ) diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/FiltersBottomSheet.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslist/FiltersBottomSheet.kt similarity index 96% rename from app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/FiltersBottomSheet.kt rename to app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslist/FiltersBottomSheet.kt index eee12365b..5dd0a7604 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/FiltersBottomSheet.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslist/FiltersBottomSheet.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -package org.equeim.tremotesf.ui.torrentslistfragment +package org.equeim.tremotesf.ui.torrentslist import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -49,11 +49,11 @@ import org.equeim.tremotesf.ui.SortAscending import org.equeim.tremotesf.ui.SortDescending import org.equeim.tremotesf.ui.components.TremotesfComboBox import org.equeim.tremotesf.ui.components.TremotesfIconButtonWithTooltip -import org.equeim.tremotesf.ui.torrentslistfragment.TorrentsListFragmentViewModel.Companion.statusFilterAcceptsTorrent -import org.equeim.tremotesf.ui.torrentslistfragment.TorrentsListFragmentViewModel.SortAndFilterSettings -import org.equeim.tremotesf.ui.torrentslistfragment.TorrentsListFragmentViewModel.SortMode -import org.equeim.tremotesf.ui.torrentslistfragment.TorrentsListFragmentViewModel.SortOrder -import org.equeim.tremotesf.ui.torrentslistfragment.TorrentsListFragmentViewModel.StatusFilterMode +import org.equeim.tremotesf.ui.torrentslist.TorrentsListScreenViewModel.Companion.statusFilterAcceptsTorrent +import org.equeim.tremotesf.ui.torrentslist.TorrentsListScreenViewModel.SortAndFilterSettings +import org.equeim.tremotesf.ui.torrentslist.TorrentsListScreenViewModel.SortMode +import org.equeim.tremotesf.ui.torrentslist.TorrentsListScreenViewModel.SortOrder +import org.equeim.tremotesf.ui.torrentslist.TorrentsListScreenViewModel.StatusFilterMode import org.equeim.tremotesf.ui.utils.rememberAlphanumericComparator import java.util.function.BiFunction diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/ServerStatsDialogFragment.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslist/ServerStatsDialogScreen.kt similarity index 89% rename from app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/ServerStatsDialogFragment.kt rename to app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslist/ServerStatsDialogScreen.kt index 6978b853b..827fd8e17 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/ServerStatsDialogFragment.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslist/ServerStatsDialogScreen.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -package org.equeim.tremotesf.ui.torrentslistfragment +package org.equeim.tremotesf.ui.torrentslist import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -19,15 +19,18 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.ViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavController +import androidx.navigation3.runtime.metadata +import androidx.navigation3.scene.DialogSceneStrategy.Companion.DialogKey import com.cheonjaeung.compose.grid.GridScope import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.StateFlow +import kotlinx.parcelize.Parcelize import org.equeim.tremotesf.R import org.equeim.tremotesf.rpc.GlobalRpcClient import org.equeim.tremotesf.rpc.RpcRequestState @@ -37,8 +40,9 @@ import org.equeim.tremotesf.rpc.requests.SessionStatsResponseArguments import org.equeim.tremotesf.rpc.requests.TransferRate import org.equeim.tremotesf.rpc.requests.getDownloadDirFreeSpace import org.equeim.tremotesf.rpc.requests.getSessionStats -import org.equeim.tremotesf.ui.ComposeDialogFragment +import org.equeim.tremotesf.ui.Destination import org.equeim.tremotesf.ui.Dimens +import org.equeim.tremotesf.ui.NavController import org.equeim.tremotesf.ui.ScreenPreview import org.equeim.tremotesf.ui.components.TremotesfAlertDialogContent import org.equeim.tremotesf.ui.components.TremotesfDetailsGrid @@ -52,20 +56,24 @@ import java.text.DecimalFormat import java.text.NumberFormat import kotlin.time.Duration.Companion.days -class ServerStatsDialogFragment : ComposeDialogFragment() { +@Parcelize +data object ServerStatsDialogDestination : Destination { @Composable override fun Content(navController: NavController) { - val model = viewModel() + val model = viewModel() ServerStatsDialogContent( uiState = model.uiState.collectAsStateWithLifecycle().value, - onDismissRequest = ::dismiss + onDismissRequest = navController::popBackStack ) } + + override val metadata: Map + get() = metadata { put(DialogKey, DialogProperties()) } } @Composable private fun ServerStatsDialogContent( - uiState: RpcRequestState, + uiState: RpcRequestState, onDismissRequest: () -> Unit ) { TremotesfAlertDialogContent( @@ -139,7 +147,7 @@ private fun ServerStatsDialogPreview() = ScreenPreview { ServerStatsDialogContent( uiState = remember { RpcRequestState.Loaded( - ServerStatsDialogFragmentViewModel.UiState( + ServerStatsDialogScreenViewModel.UiState( sessionStats = SessionStatsResponseArguments( downloadSpeed = TransferRate.fromKiloBytesPerSecond(666), uploadSpeed = TransferRate.fromKiloBytesPerSecond(7777777), @@ -164,7 +172,7 @@ private fun ServerStatsDialogPreview() = ScreenPreview { ) } -class ServerStatsDialogFragmentViewModel : ViewModel() { +class ServerStatsDialogScreenViewModel : ViewModel() { data class UiState( val sessionStats: SessionStatsResponseArguments, val downloadDirFreeSpace: FileSize diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/TorrentsList.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslist/TorrentsList.kt similarity index 99% rename from app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/TorrentsList.kt rename to app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslist/TorrentsList.kt index 4765b0cff..96769d5a2 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/TorrentsList.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslist/TorrentsList.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -package org.equeim.tremotesf.ui.torrentslistfragment +package org.equeim.tremotesf.ui.torrentslist import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -111,7 +111,7 @@ fun TorrentsList( multilineName: Boolean, toolbarClicked: Flow, labelsEnabled: State, - sortAndFilterSettings: State, + sortAndFilterSettings: State, torrentsOperations: TorrentsOperations, navigateToTorrentPropertiesScreen: (torrentHashString: String) -> Unit, navigateToSetLocationDialog: (torrentHashStrings: List, location: String) -> Unit, diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/TorrentsListFragment.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslist/TorrentsListScreen.kt similarity index 87% rename from app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/TorrentsListFragment.kt rename to app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslist/TorrentsListScreen.kt index e56380aba..83d587f71 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/TorrentsListFragment.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslist/TorrentsListScreen.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -package org.equeim.tremotesf.ui.torrentslistfragment +package org.equeim.tremotesf.ui.torrentslist import android.Manifest import android.net.Uri @@ -52,39 +52,48 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavController import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first +import kotlinx.parcelize.Parcelize import org.equeim.tremotesf.R import org.equeim.tremotesf.rpc.GlobalRpcClient import org.equeim.tremotesf.rpc.GlobalServers import org.equeim.tremotesf.rpc.RpcRequestState import org.equeim.tremotesf.rpc.isRecoverable import org.equeim.tremotesf.rpc.requests.Torrent -import org.equeim.tremotesf.ui.ComposeFragment +import org.equeim.tremotesf.ui.AboutDestination +import org.equeim.tremotesf.ui.Destination import org.equeim.tremotesf.ui.Dimens +import org.equeim.tremotesf.ui.LabelsEditDialogDestination +import org.equeim.tremotesf.ui.NavController +import org.equeim.tremotesf.ui.SettingsDestination import org.equeim.tremotesf.ui.ShowRpcErrorsSnackbar +import org.equeim.tremotesf.ui.TorrentsSetLocationDialogDestination +import org.equeim.tremotesf.ui.addtorrent.AddTorrentFileDestination +import org.equeim.tremotesf.ui.addtorrent.AddTorrentLinkDestination import org.equeim.tremotesf.ui.addtorrent.MergingTrackersMessage import org.equeim.tremotesf.ui.addtorrent.ShowMergingTrackersMessage import org.equeim.tremotesf.ui.components.TremotesfIconButtonWithTooltipAndMenu import org.equeim.tremotesf.ui.components.TremotesfRuntimePermissionHelper import org.equeim.tremotesf.ui.components.TremotesfScreenContentWithPlaceholder import org.equeim.tremotesf.ui.components.rememberTremotesfRuntimePermissionHelperState -import org.equeim.tremotesf.ui.torrentslistfragment.TorrentsListFragmentViewModel.FloatingActionButtonState +import org.equeim.tremotesf.ui.connectionsettings.ConnectionSettingsDestination +import org.equeim.tremotesf.ui.connectionsettings.ServerEditDestination +import org.equeim.tremotesf.ui.serversettings.ServerSettingsDestination +import org.equeim.tremotesf.ui.torrentproperties.TorrentPropertiesDestination +import org.equeim.tremotesf.ui.torrentslist.TorrentsListScreenViewModel.FloatingActionButtonState import org.equeim.tremotesf.ui.utils.Utils import org.equeim.tremotesf.ui.utils.rememberFileSizeFormatter -import org.equeim.tremotesf.ui.utils.safeNavigate -class TorrentsListFragment : ComposeFragment() { +@Parcelize +data object TorrentsListDestination : Destination { @Composable override fun Content(navController: NavController) { - // Need to scope it to the back stack entry instead of fragment since it will be accessed from AddTorrent(File|Link)Fragment - val model = - viewModel(viewModelStoreOwner = navController.getBackStackEntry(R.id.torrents_list_fragment)) + val model = viewModel() val context = LocalContext.current TorrentsListScreen( title = model.titleState.collectAsStateWithLifecycle(), @@ -105,42 +114,26 @@ class TorrentsListFragment : ComposeFragment() { sortAndFilterSettings = model.sortAndFilterSettings.collectAsStateWithLifecycle(), floatingActionButtonState = model.floatingActionButtonState.collectAsStateWithLifecycle(), - navigateToSettings = { navController.safeNavigate(TorrentsListFragmentDirections.toSettingsFragment()) }, - navigateToAboutScreen = { navController.safeNavigate(TorrentsListFragmentDirections.toAboutFragment()) }, + navigateToSettings = { navController.navigateTo(SettingsDestination) }, + navigateToAboutScreen = { navController.navigateTo(AboutDestination) }, shutdownApp = { Utils.shutdownApp(context) }, - navigateToServerAddingScreen = { navController.safeNavigate(TorrentsListFragmentDirections.toServerEditFragment()) }, - navigateToAddTorrentFileScreen = { - navController.safeNavigate( - TorrentsListFragmentDirections.toAddTorrentFileFragment( - it - ) - ) + navigateToServerAddingScreen = { navController.navigateTo(ServerEditDestination()) }, + navigateToAddTorrentFileScreen = { navController.navigateTo(AddTorrentFileDestination(it)) }, + navigateToAddTorrentLinkScreen = { + navController.navigateTo(AddTorrentLinkDestination()) }, - navigateToAddTorrentLinkScreen = { navController.safeNavigate(TorrentsListFragmentDirections.toAddTorrentLinkFragment()) }, - navigateToTorrentPropertiesScreen = { - navController.safeNavigate( - TorrentsListFragmentDirections.toTorrentPropertiesFragment( - it - ) - ) - }, - navigateToConnectionSettingsScreen = { navController.safeNavigate(TorrentsListFragmentDirections.toConnectionSettingsFragment()) }, - navigateToServerSettingsScreen = { navController.safeNavigate(TorrentsListFragmentDirections.toServerSettingsFragment()) }, - navigateToServerStatsDialog = { navController.safeNavigate(TorrentsListFragmentDirections.toServerStatsDialog()) }, + navigateToTorrentPropertiesScreen = { navController.navigateTo(TorrentPropertiesDestination(it)) }, + navigateToConnectionSettingsScreen = { navController.navigateTo(ConnectionSettingsDestination) }, + navigateToServerSettingsScreen = { navController.navigateTo(ServerSettingsDestination) }, + navigateToServerStatsDialog = { navController.navigateTo(ServerStatsDialogDestination) }, navigateToSetLocationDialog = { torrentHashStrings, location -> - navController.safeNavigate( - TorrentsListFragmentDirections.toTorrentsSetLocationDialog( - torrentHashStrings.toTypedArray(), - location - ) + navController.navigateTo( + TorrentsSetLocationDialogDestination(torrentHashStrings, location) ) }, navigateToLabelsEditDialog = { torrentHashStrings, enabledLabels -> - navController.safeNavigate( - TorrentsListFragmentDirections.toLabelsEditDialog( - torrentHashStrings.toTypedArray(), - enabledLabels.toTypedArray() - ) + navController.navigateTo( + LabelsEditDialogDestination(torrentHashStrings, enabledLabels) ) }, @@ -165,8 +158,8 @@ class TorrentsListFragment : ComposeFragment() { @OptIn(ExperimentalMaterial3Api::class) @Composable private fun TorrentsListScreen( - title: State, - subtitle: State, + title: State, + subtitle: State, shouldConnectToServer: State, setShouldConnectToServer: (Boolean) -> Unit, @@ -178,7 +171,7 @@ private fun TorrentsListScreen( labelsEnabled: State, - sortAndFilterSettings: State, + sortAndFilterSettings: State, showTransmissionSettingsButton: State, showFiltersAndSearchButtons: State, floatingActionButtonState: State, @@ -200,7 +193,7 @@ private fun TorrentsListScreen( allTorrents: State>>, refreshingManually: State, refreshManually: () -> Unit, - listSettings: State, + listSettings: State, quickReturnEnabled: State, torrentsOperations: TorrentsOperations, @@ -429,6 +422,7 @@ private fun ShowNotificationPermissionSnackbar( withDismissAction = true, duration = SnackbarDuration.Indefinite ) + showSnackbar = false onShownNotificationPermissionRequest() if (result == SnackbarResult.ActionPerformed) { notificationPermissionHelperState.requestPermission() diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/TorrentsListFragmentViewModel.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslist/TorrentsListScreenViewModel.kt similarity index 99% rename from app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/TorrentsListFragmentViewModel.kt rename to app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslist/TorrentsListScreenViewModel.kt index 388b497c8..d3f15b15a 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/TorrentsListFragmentViewModel.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslist/TorrentsListScreenViewModel.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -package org.equeim.tremotesf.ui.torrentslistfragment +package org.equeim.tremotesf.ui.torrentslist import android.app.Application import androidx.annotation.StringRes @@ -72,7 +72,7 @@ import java.time.Instant import java.util.Locale import kotlin.time.Duration -class TorrentsListFragmentViewModel(application: Application, savedStateHandle: SavedStateHandle) : +class TorrentsListScreenViewModel(application: Application, savedStateHandle: SavedStateHandle) : AndroidViewModel(application) { data class TitleState(val serverName: String, val serverAddress: String) { constructor(server: Server) : this(serverName = server.name, serverAddress = server.address) diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/TransmissionSettingsBottomSheet.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslist/TransmissionSettingsBottomSheet.kt similarity index 99% rename from app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/TransmissionSettingsBottomSheet.kt rename to app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslist/TransmissionSettingsBottomSheet.kt index 3b703a304..de2b2a99b 100644 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/TransmissionSettingsBottomSheet.kt +++ b/app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslist/TransmissionSettingsBottomSheet.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -package org.equeim.tremotesf.ui.torrentslistfragment +package org.equeim.tremotesf.ui.torrentslist import androidx.annotation.StringRes import androidx.compose.foundation.clickable diff --git a/app/src/main/kotlin/org/equeim/tremotesf/ui/utils/SafeNavigate.kt b/app/src/main/kotlin/org/equeim/tremotesf/ui/utils/SafeNavigate.kt deleted file mode 100644 index 5f3846896..000000000 --- a/app/src/main/kotlin/org/equeim/tremotesf/ui/utils/SafeNavigate.kt +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-FileCopyrightText: 2017-2026 Alexey Rochev -// -// SPDX-License-Identifier: GPL-3.0-or-later - -package org.equeim.tremotesf.ui.utils - -import androidx.navigation.NavController -import androidx.navigation.NavDirections -import androidx.navigation.NavOptions -import timber.log.Timber - -fun NavController.safeNavigate(directions: NavDirections, navOptions: NavOptions? = null) { - try { - navigate(directions, navOptions) - } catch (e: IllegalArgumentException) { - Timber.e(e, "Failed to navigate") - } -} diff --git a/app/src/main/res/animator/nav_default_enter_anim.xml b/app/src/main/res/animator/nav_default_enter_anim.xml deleted file mode 100644 index 26f0b43ff..000000000 --- a/app/src/main/res/animator/nav_default_enter_anim.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - \ No newline at end of file diff --git a/app/src/main/res/animator/nav_default_exit_anim.xml b/app/src/main/res/animator/nav_default_exit_anim.xml deleted file mode 100644 index 5ac32e2be..000000000 --- a/app/src/main/res/animator/nav_default_exit_anim.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - \ No newline at end of file diff --git a/app/src/main/res/animator/nav_default_pop_enter_anim.xml b/app/src/main/res/animator/nav_default_pop_enter_anim.xml deleted file mode 100644 index 26f0b43ff..000000000 --- a/app/src/main/res/animator/nav_default_pop_enter_anim.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - \ No newline at end of file diff --git a/app/src/main/res/animator/nav_default_pop_exit_anim.xml b/app/src/main/res/animator/nav_default_pop_exit_anim.xml deleted file mode 100644 index 5ac32e2be..000000000 --- a/app/src/main/res/animator/nav_default_pop_exit_anim.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - \ No newline at end of file diff --git a/app/src/main/res/layout/navigation_activity.xml b/app/src/main/res/layout/navigation_activity.xml deleted file mode 100644 index d2eaf9f99..000000000 --- a/app/src/main/res/layout/navigation_activity.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - diff --git a/app/src/main/res/navigation/nav_main.xml b/app/src/main/res/navigation/nav_main.xml deleted file mode 100644 index f39336e11..000000000 --- a/app/src/main/res/navigation/nav_main.xml +++ /dev/null @@ -1,246 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml deleted file mode 100644 index 93d6fe2c3..000000000 --- a/app/src/main/res/values/dimens.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - 300 - diff --git a/build.gradle.kts b/build.gradle.kts index df84481ce..7000fa9e3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,6 @@ plugins { alias(libs.plugins.kotlin.plugin.compose) apply (false) alias(libs.plugins.kotlin.plugin.serialization) apply (false) - alias(libs.plugins.androidx.navigation) apply (false) alias(libs.plugins.tremotesf.common.settings) apply(false) alias(libs.plugins.deezer.caupain) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c1be43a0b..df38b98e3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,9 +19,8 @@ androidx-annotation = "1.10.0" androidx-collection = "1.6.0" androidx-compose = "2026.06.01" androidx-core = "1.19.0" -androidx-fragment = "1.8.9" androidx-lifecycle = "2.11.0" -androidx-navigation = "2.9.8" +androidx-navigation3 = "1.1.5" androidx-webkit = "1.16.0" androidx-window = "1.5.1" androidx-work = "2.11.2" @@ -61,12 +60,12 @@ androidx-compose-material3 = { module = "androidx.compose.material3:material3" } androidx-compose-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive" } androidx-compose-materialIconsCore = { module = "androidx.compose.material:material-icons-core" } androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } -androidx-fragment = { module = "androidx.fragment:fragment-ktx", version.ref = "androidx-fragment" } androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } androidx-lifecycle-service = { module = "androidx.lifecycle:lifecycle-service", version.ref = "androidx-lifecycle" } androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } androidx-lifecycle-viewmodel-savedstate = { module = "androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "androidx-lifecycle" } -androidx-navigation-fragment = { module = "androidx.navigation:navigation-fragment", version.ref = "androidx-navigation" } +androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "androidx-navigation3" } +androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "androidx-navigation3" } androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "androidx-webkit" } androidx-window = { module = "androidx.window:window", version.ref = "androidx-window" } androidx-work-runtime = { module = "androidx.work:work-runtime", version.ref = "androidx-work" } @@ -92,6 +91,5 @@ android-library = { id = "com.android.library" } kotlin-plugin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize" } kotlin-plugin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlin-plugin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } -androidx-navigation = { id = "androidx.navigation.safeargs.kotlin", version.ref = "androidx-navigation" } deezer-caupain = { id = "com.deezer.caupain", version.ref = "deezer-caupain" } tremotesf-common-settings = { id = "org.equeim.tremotesf.common-settings" }