Migrate More screen to Compose (#6990)

This commit is contained in:
arkon 2022-04-23 15:51:50 -04:00 committed by GitHub
parent 8933b41937
commit c25cffafc6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 341 additions and 175 deletions

View file

@ -0,0 +1,38 @@
package eu.kanade.core.prefs
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import com.fredporciuncula.flow.preferences.Preference
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
class PreferenceMutableState<T>(
private val preference: Preference<T>,
scope: CoroutineScope,
) : MutableState<T> {
private val state = mutableStateOf(preference.get())
init {
preference.asFlow()
.distinctUntilChanged()
.onEach { state.value = it }
.launchIn(scope)
}
override var value: T
get() = state.value
set(value) {
preference.set(value)
}
override fun component1(): T {
return state.value
}
override fun component2(): (T) -> Unit {
return { preference.set(it) }
}
}

View file

@ -11,12 +11,14 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.unit.dp
import eu.kanade.core.prefs.PreferenceMutableState
import eu.kanade.presentation.util.horizontalPadding
@Composable
@ -29,7 +31,7 @@ fun Divider() {
@Composable
fun PreferenceRow(
title: String,
icon: ImageVector? = null,
painter: Painter? = null,
onClick: () -> Unit = {},
onLongClick: () -> Unit = {},
subtitle: String? = null,
@ -50,18 +52,18 @@ fun PreferenceRow(
.heightIn(min = height)
.combinedClickable(
onLongClick = onLongClick,
onClick = onClick
onClick = onClick,
),
verticalAlignment = Alignment.CenterVertically
) {
if (icon != null) {
if (painter != null) {
Icon(
imageVector = icon,
painter = painter,
modifier = Modifier
.padding(horizontal = horizontalPadding)
.size(24.dp),
tint = MaterialTheme.colorScheme.primary,
contentDescription = null
contentDescription = null,
)
}
Column(
@ -88,3 +90,23 @@ fun PreferenceRow(
}
}
}
@Composable
fun SwitchPreference(
preference: PreferenceMutableState<Boolean>,
title: String,
subtitle: String? = null,
painter: Painter? = null,
) {
PreferenceRow(
title = title,
subtitle = subtitle,
painter = painter,
action = {
Switch(checked = preference.value, onCheckedChange = null)
// TODO: remove this once switch checked state is fixed: https://issuetracker.google.com/issues/228336571
Text(preference.value.toString())
},
onClick = { preference.value = !preference.value },
)
}

View file

@ -0,0 +1,131 @@
package eu.kanade.presentation.more
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.CloudOff
import androidx.compose.material.icons.outlined.GetApp
import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.outlined.Label
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.SettingsBackupRestore
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.components.Divider
import eu.kanade.presentation.components.PreferenceRow
import eu.kanade.presentation.components.SwitchPreference
import eu.kanade.presentation.util.quantityStringResource
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.more.DownloadQueueState
import eu.kanade.tachiyomi.ui.more.MoreController
import eu.kanade.tachiyomi.ui.more.MorePresenter
@Composable
fun MoreScreen(
nestedScrollInterop: NestedScrollConnection,
presenter: MorePresenter,
onClickDownloadQueue: () -> Unit,
onClickCategories: () -> Unit,
onClickBackupAndRestore: () -> Unit,
onClickSettings: () -> Unit,
onClickAbout: () -> Unit,
) {
val uriHandler = LocalUriHandler.current
val downloadQueueState by presenter.downloadQueueState.collectAsState()
LazyColumn(
modifier = Modifier.nestedScroll(nestedScrollInterop),
) {
item {
LogoHeader()
}
item {
SwitchPreference(
preference = presenter.downloadedOnly,
title = stringResource(R.string.label_downloaded_only),
subtitle = stringResource(R.string.downloaded_only_summary),
painter = rememberVectorPainter(Icons.Outlined.CloudOff),
)
}
item {
SwitchPreference(
preference = presenter.incognitoMode,
title = stringResource(R.string.pref_incognito_mode),
subtitle = stringResource(R.string.pref_incognito_mode_summary),
painter = painterResource(R.drawable.ic_glasses_24dp),
)
}
item { Divider() }
item {
PreferenceRow(
title = stringResource(R.string.label_download_queue),
subtitle = when (downloadQueueState) {
DownloadQueueState.Stopped -> null
is DownloadQueueState.Paused -> {
val pending = (downloadQueueState as DownloadQueueState.Paused).pending
if (pending == 0) {
stringResource(R.string.paused)
} else {
"${stringResource(R.string.paused)}${quantityStringResource(R.plurals.download_queue_summary, pending, pending)}"
}
}
is DownloadQueueState.Downloading -> {
val pending = (downloadQueueState as DownloadQueueState.Downloading).pending
quantityStringResource(R.plurals.download_queue_summary, pending, pending)
}
},
painter = rememberVectorPainter(Icons.Outlined.GetApp),
onClick = { onClickDownloadQueue() },
)
}
item {
PreferenceRow(
title = stringResource(R.string.categories),
painter = rememberVectorPainter(Icons.Outlined.Label),
onClick = { onClickCategories() },
)
}
item {
PreferenceRow(
title = stringResource(R.string.label_backup),
painter = rememberVectorPainter(Icons.Outlined.SettingsBackupRestore),
onClick = { onClickBackupAndRestore() },
)
}
item { Divider() }
item {
PreferenceRow(
title = stringResource(R.string.label_settings),
painter = rememberVectorPainter(Icons.Outlined.Settings),
onClick = { onClickSettings() },
)
}
item {
PreferenceRow(
title = stringResource(R.string.pref_category_about),
painter = rememberVectorPainter(Icons.Outlined.Info),
onClick = { onClickAbout() },
)
}
item {
PreferenceRow(
title = stringResource(R.string.label_help),
painter = rememberVectorPainter(Icons.Outlined.HelpOutline),
onClick = { uriHandler.openUri(MoreController.URL_HELP) },
)
}
}
}

View file

@ -0,0 +1,32 @@
package eu.kanade.presentation.util
import androidx.annotation.PluralsRes
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
/**
* Load a quantity string resource.
*
* @param id the resource identifier
* @param quantity The number used to get the string for the current language's plural rules.
* @return the string data associated with the resource
*/
@Composable
fun quantityStringResource(@PluralsRes id: Int, quantity: Int): String {
val context = LocalContext.current
return context.resources.getQuantityString(id, quantity, quantity)
}
/**
* Load a quantity string resource with formatting.
*
* @param id the resource identifier
* @param quantity The number used to get the string for the current language's plural rules.
* @param formatArgs the format arguments
* @return the string data associated with the resource
*/
@Composable
fun quantityStringResource(@PluralsRes id: Int, quantity: Int, vararg formatArgs: Any): String {
val context = LocalContext.current
return context.resources.getQuantityString(id, quantity, *formatArgs)
}

View file

@ -1,6 +1,8 @@
package eu.kanade.tachiyomi.ui.base.presenter
import android.os.Bundle
import com.fredporciuncula.flow.preferences.Preference
import eu.kanade.core.prefs.PreferenceMutableState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
@ -10,7 +12,7 @@ import rx.Observable
open class BasePresenter<V> : RxPresenter<V>() {
lateinit var presenterScope: CoroutineScope
var presenterScope: CoroutineScope = MainScope()
/**
* Query from the view where applicable
@ -20,7 +22,6 @@ open class BasePresenter<V> : RxPresenter<V>() {
override fun onCreate(savedState: Bundle?) {
try {
super.onCreate(savedState)
presenterScope = MainScope()
} catch (e: NullPointerException) {
// Swallow this error. This should be fixed in the library but since it's not critical
// (only used by restartables) it should be enough. It saves me a fork.
@ -38,6 +39,8 @@ open class BasePresenter<V> : RxPresenter<V>() {
return super.getView()
}
fun <T> Preference<T>.asState() = PreferenceMutableState(this, presenterScope)
/**
* Subscribes an observable with [deliverFirst] and adds it to the presenter's lifecycle
* subscription list.

View file

@ -1,187 +1,38 @@
package eu.kanade.tachiyomi.ui.more
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.preference.Preference
import androidx.preference.PreferenceScreen
import androidx.compose.runtime.Composable
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import eu.kanade.presentation.more.MoreScreen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.ui.base.controller.ComposeController
import eu.kanade.tachiyomi.ui.base.controller.NoAppBarElevationController
import eu.kanade.tachiyomi.ui.base.controller.RootController
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.category.CategoryController
import eu.kanade.tachiyomi.ui.download.DownloadController
import eu.kanade.tachiyomi.ui.setting.SettingsBackupController
import eu.kanade.tachiyomi.ui.setting.SettingsController
import eu.kanade.tachiyomi.ui.setting.SettingsMainController
import eu.kanade.tachiyomi.util.preference.add
import eu.kanade.tachiyomi.util.preference.bindTo
import eu.kanade.tachiyomi.util.preference.iconRes
import eu.kanade.tachiyomi.util.preference.iconTint
import eu.kanade.tachiyomi.util.preference.onClick
import eu.kanade.tachiyomi.util.preference.preference
import eu.kanade.tachiyomi.util.preference.preferenceCategory
import eu.kanade.tachiyomi.util.preference.summaryRes
import eu.kanade.tachiyomi.util.preference.switchPreference
import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.openInBrowser
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.subscriptions.CompositeSubscription
import uy.kohesive.injekt.injectLazy
class MoreController :
SettingsController(),
ComposeController<MorePresenter>(),
RootController,
NoAppBarElevationController {
private val downloadManager: DownloadManager by injectLazy()
private var isDownloading: Boolean = false
private var downloadQueueSize: Int = 0
override fun getTitle() = resources?.getString(R.string.label_more)
private var untilDestroySubscriptions = CompositeSubscription()
private set
override fun createPresenter() = MorePresenter()
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.label_more
val tintColor = context.getResourceColor(R.attr.colorAccent)
add(MoreHeaderPreference(context))
switchPreference {
bindTo(preferences.downloadedOnly())
titleRes = R.string.label_downloaded_only
summaryRes = R.string.downloaded_only_summary
iconRes = R.drawable.ic_cloud_off_24dp
iconTint = tintColor
}
switchPreference {
bindTo(preferences.incognitoMode())
summaryRes = R.string.pref_incognito_mode_summary
titleRes = R.string.pref_incognito_mode
iconRes = R.drawable.ic_glasses_24dp
iconTint = tintColor
preferences.incognitoMode().asFlow()
.onEach { isChecked = it }
.launchIn(viewScope)
}
preferenceCategory {
preference {
titleRes = R.string.label_download_queue
if (downloadManager.queue.isNotEmpty()) {
initDownloadQueueSummary(this)
}
iconRes = R.drawable.ic_get_app_24dp
iconTint = tintColor
onClick {
router.pushController(DownloadController())
}
}
preference {
titleRes = R.string.categories
iconRes = R.drawable.ic_label_24dp
iconTint = tintColor
onClick {
router.pushController(CategoryController())
}
}
preference {
titleRes = R.string.label_backup
iconRes = R.drawable.ic_settings_backup_restore_24dp
iconTint = tintColor
onClick {
router.pushController(SettingsBackupController())
}
}
}
preferenceCategory {
preference {
titleRes = R.string.label_settings
iconRes = R.drawable.ic_settings_24dp
iconTint = tintColor
onClick {
router.pushController(SettingsMainController())
}
}
preference {
iconRes = R.drawable.ic_info_24dp
iconTint = tintColor
titleRes = R.string.pref_category_about
onClick {
router.pushController(AboutController())
}
}
preference {
titleRes = R.string.label_help
iconRes = R.drawable.ic_help_24dp
iconTint = tintColor
onClick {
activity?.openInBrowser(URL_HELP)
}
}
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle?): View {
if (untilDestroySubscriptions.isUnsubscribed) {
untilDestroySubscriptions = CompositeSubscription()
}
return super.onCreateView(inflater, container, savedInstanceState)
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
untilDestroySubscriptions.unsubscribe()
}
private fun initDownloadQueueSummary(preference: Preference) {
// Handle running/paused status change
DownloadService.runningRelay
.observeOn(AndroidSchedulers.mainThread())
.subscribeUntilDestroy { isRunning ->
isDownloading = isRunning
updateDownloadQueueSummary(preference)
}
// Handle queue progress updating
downloadManager.queue.getUpdatedObservable()
.observeOn(AndroidSchedulers.mainThread())
.subscribeUntilDestroy {
downloadQueueSize = it.size
updateDownloadQueueSummary(preference)
}
}
private fun updateDownloadQueueSummary(preference: Preference) {
var pendingDownloadExists = downloadQueueSize != 0
var pauseMessage = resources?.getString(R.string.paused)
var numberOfPendingDownloads = resources?.getQuantityString(R.plurals.download_queue_summary, downloadQueueSize, downloadQueueSize)
preference.summary = when {
!pendingDownloadExists -> null
!isDownloading && !pendingDownloadExists -> pauseMessage
!isDownloading && pendingDownloadExists -> "$pauseMessage$numberOfPendingDownloads"
else -> numberOfPendingDownloads
}
}
private fun <T> Observable<T>.subscribeUntilDestroy(onNext: (T) -> Unit): Subscription {
return subscribe(onNext).also { untilDestroySubscriptions.add(it) }
@Composable
override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) {
MoreScreen(
nestedScrollInterop = nestedScrollInterop,
presenter = presenter,
onClickDownloadQueue = { router.pushController(DownloadController()) },
onClickCategories = { router.pushController(CategoryController()) },
onClickBackupAndRestore = { router.pushController(SettingsBackupController()) },
onClickSettings = { router.pushController(SettingsMainController()) },
onClickAbout = { router.pushController(AboutController()) },
)
}
companion object {

View file

@ -0,0 +1,89 @@
package eu.kanade.tachiyomi.ui.more
import android.os.Bundle
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.subscriptions.CompositeSubscription
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MorePresenter(
private val downloadManager: DownloadManager = Injekt.get(),
preferencesHelper: PreferencesHelper = Injekt.get(),
) : BasePresenter<MoreController>() {
val downloadedOnly = preferencesHelper.downloadedOnly().asState()
val incognitoMode = preferencesHelper.incognitoMode().asState()
private var _state: MutableStateFlow<DownloadQueueState> = MutableStateFlow(DownloadQueueState.Stopped)
val downloadQueueState: StateFlow<DownloadQueueState> = _state
private var isDownloading: Boolean = false
private var downloadQueueSize: Int = 0
private var untilDestroySubscriptions = CompositeSubscription()
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
if (untilDestroySubscriptions.isUnsubscribed) {
untilDestroySubscriptions = CompositeSubscription()
}
initDownloadQueueSummary()
}
override fun onDestroy() {
super.onDestroy()
untilDestroySubscriptions.unsubscribe()
}
private fun initDownloadQueueSummary() {
// Handle running/paused status change
DownloadService.runningRelay
.observeOn(AndroidSchedulers.mainThread())
.subscribeUntilDestroy { isRunning ->
isDownloading = isRunning
updateDownloadQueueState()
}
// Handle queue progress updating
downloadManager.queue.getUpdatedObservable()
.observeOn(AndroidSchedulers.mainThread())
.subscribeUntilDestroy {
downloadQueueSize = it.size
updateDownloadQueueState()
}
}
private fun updateDownloadQueueState() {
presenterScope.launchIO {
val pendingDownloadExists = downloadQueueSize != 0
_state.emit(
when {
!pendingDownloadExists -> DownloadQueueState.Stopped
!isDownloading && !pendingDownloadExists -> DownloadQueueState.Paused(0)
!isDownloading && pendingDownloadExists -> DownloadQueueState.Paused(downloadQueueSize)
else -> DownloadQueueState.Downloading(downloadQueueSize)
}
)
}
}
private fun <T> Observable<T>.subscribeUntilDestroy(onNext: (T) -> Unit): Subscription {
return subscribe(onNext).also { untilDestroySubscriptions.add(it) }
}
}
sealed class DownloadQueueState {
object Stopped : DownloadQueueState()
data class Paused(val pending: Int) : DownloadQueueState()
data class Downloading(val pending: Int) : DownloadQueueState()
}