refactor: Use Compose for About page

This commit is contained in:
Ahmad Ansori Palembani 2024-12-28 10:53:01 +07:00
parent 37f1f0e330
commit cab40214d2
Signed by: null2264
GPG key ID: BA64F8B60AF3EFB6
19 changed files with 571 additions and 301 deletions

View file

@ -21,6 +21,7 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co
- Refactor EmptyView to use Compose - Refactor EmptyView to use Compose
- Refactor Reader ChapterTransition to use Compose (@arkon) - Refactor Reader ChapterTransition to use Compose (@arkon)
- [Experimental] Add modified version of LargeTopAppBar that mimic J2K's ExpandedAppBarLayout - [Experimental] Add modified version of LargeTopAppBar that mimic J2K's ExpandedAppBarLayout
- Refactor About page to use Compose
## [1.9.7] ## [1.9.7]

View file

@ -5,9 +5,17 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import eu.kanade.tachiyomi.core.preference.Preference import eu.kanade.tachiyomi.core.preference.Preference
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Locale
@Composable @Composable
fun <T> Preference<T>.collectAsState(): State<T> { fun <T> Preference<T>.collectAsState(): State<T> {
val flow = remember(this) { changes() } val flow = remember(this) { changes() }
return flow.collectAsState(initial = get()) return flow.collectAsState(initial = get())
} }
fun String.asDateFormat(): DateFormat = when (this) {
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
else -> SimpleDateFormat(this, Locale.getDefault())
}

View file

@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.core.preference.Preference import eu.kanade.tachiyomi.core.preference.Preference
import eu.kanade.tachiyomi.core.preference.PreferenceStore import eu.kanade.tachiyomi.core.preference.PreferenceStore
import eu.kanade.tachiyomi.core.preference.getEnum import eu.kanade.tachiyomi.core.preference.getEnum
import eu.kanade.tachiyomi.core.storage.preference.asDateFormat
import eu.kanade.tachiyomi.data.updater.AppDownloadInstallJob import eu.kanade.tachiyomi.data.updater.AppDownloadInstallJob
import eu.kanade.tachiyomi.domain.manga.models.Manga import eu.kanade.tachiyomi.domain.manga.models.Manga
import eu.kanade.tachiyomi.extension.model.InstalledExtensionsOrder import eu.kanade.tachiyomi.extension.model.InstalledExtensionsOrder
@ -19,13 +20,12 @@ import eu.kanade.tachiyomi.ui.reader.settings.ReadingModeType
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
import eu.kanade.tachiyomi.ui.recents.RecentsPresenter import eu.kanade.tachiyomi.ui.recents.RecentsPresenter
import eu.kanade.tachiyomi.util.system.Themes import eu.kanade.tachiyomi.util.system.Themes
import java.text.DateFormat
import java.util.Locale
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
@ -187,10 +187,10 @@ class PreferencesHelper(val context: Context, val preferenceStore: PreferenceSto
fun anilistScoreType() = preferenceStore.getString("anilist_score_type", "POINT_10") fun anilistScoreType() = preferenceStore.getString("anilist_score_type", "POINT_10")
fun dateFormat(format: String = preferenceStore.getString(Keys.dateFormat, "").get()): DateFormat = when (format) { fun dateFormatRaw() = preferenceStore.getString(Keys.dateFormat, "")
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
else -> SimpleDateFormat(format, Locale.getDefault()) @Deprecated("Use dateFormatRaw().get().asDateFormat() instead")
} fun dateFormat(format: String = dateFormatRaw().get()): DateFormat = format.asDateFormat()
fun appLanguage() = preferenceStore.getString("app_language", "") fun appLanguage() = preferenceStore.getString("app_language", "")

View file

@ -1,173 +1,53 @@
package eu.kanade.tachiyomi.ui.more package eu.kanade.tachiyomi.ui.more
import android.app.Dialog import android.app.Dialog
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import androidx.core.content.getSystemService import androidx.compose.runtime.Composable
import androidx.core.net.toUri import androidx.compose.runtime.CompositionLocalProvider
import androidx.preference.PreferenceScreen import cafe.adriel.voyager.core.stack.StackEvent
import co.touchlab.kermit.Logger import cafe.adriel.voyager.navigator.Navigator
import eu.kanade.tachiyomi.BuildConfig import cafe.adriel.voyager.transitions.ScreenTransition
import eu.kanade.tachiyomi.data.updater.AppDownloadInstallJob import eu.kanade.tachiyomi.data.updater.AppDownloadInstallJob
import eu.kanade.tachiyomi.data.updater.AppUpdateChecker import eu.kanade.tachiyomi.ui.base.controller.BaseComposeController
import eu.kanade.tachiyomi.data.updater.AppUpdateNotifier
import eu.kanade.tachiyomi.data.updater.AppUpdateResult
import eu.kanade.tachiyomi.data.updater.RELEASE_URL
import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.setting.SettingsLegacyController import eu.kanade.tachiyomi.util.compose.LocalAlertDialog
import eu.kanade.tachiyomi.ui.setting.add import eu.kanade.tachiyomi.util.compose.LocalBackPress
import eu.kanade.tachiyomi.ui.setting.onClick
import eu.kanade.tachiyomi.ui.setting.preference
import eu.kanade.tachiyomi.ui.setting.preferenceCategory
import eu.kanade.tachiyomi.ui.setting.titleMRes
import eu.kanade.tachiyomi.util.CrashLogUtil
import eu.kanade.tachiyomi.util.lang.toTimestampString
import eu.kanade.tachiyomi.util.system.isOnline
import eu.kanade.tachiyomi.util.system.localeContext
import eu.kanade.tachiyomi.util.system.materialAlertDialog import eu.kanade.tachiyomi.util.system.materialAlertDialog
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.setNegativeButton import eu.kanade.tachiyomi.util.view.setNegativeButton
import eu.kanade.tachiyomi.util.view.setPositiveButton import eu.kanade.tachiyomi.util.view.setPositiveButton
import eu.kanade.tachiyomi.util.view.setTitle import eu.kanade.tachiyomi.util.view.setTitle
import eu.kanade.tachiyomi.util.view.snack
import eu.kanade.tachiyomi.util.view.withFadeTransaction
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
import kotlinx.coroutines.Dispatchers import soup.compose.material.motion.animation.materialSharedAxisZ
import kotlinx.coroutines.launch import yokai.domain.ComposableAlertDialog
import kotlinx.coroutines.withContext
import yokai.i18n.MR import yokai.i18n.MR
import yokai.util.lang.getString import yokai.presentation.settings.screen.about.AboutScreen
import java.text.DateFormat
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
import android.R as AR import android.R as AR
class AboutController : SettingsLegacyController() { class AboutController : BaseComposeController() {
/** @Composable
* Checks for new releases override fun ScreenContent() {
*/ Navigator(
private val updateChecker by lazy { AppUpdateChecker() } screen = AboutScreen { body, url, isBeta ->
NewUpdateDialogController(body, url, isBeta).showDialog(router)
private val dateFormat: DateFormat by lazy { },
preferences.dateFormat() content = {
} CompositionLocalProvider(
LocalAlertDialog provides ComposableAlertDialog(null),
private val isUpdaterEnabled = BuildConfig.INCLUDE_UPDATER LocalBackPress provides router::handleBack,
) {
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { ScreenTransition(
titleMRes = MR.strings.about navigator = it,
// FIXME: Mimic J2K's Conductor transition
preference { transition = { materialSharedAxisZ(forward = it.lastEvent != StackEvent.Pop) },
key = "pref_whats_new" )
titleMRes = MR.strings.whats_new_this_release
onClick {
val intent = Intent(
Intent.ACTION_VIEW,
if (BuildConfig.DEBUG) {
"https://github.com/null2264/yokai/commits/master"
} else {
RELEASE_URL
}.toUri(),
)
startActivity(intent)
}
}
if (isUpdaterEnabled) {
preference {
key = "pref_check_for_updates"
titleMRes = MR.strings.check_for_updates
onClick {
if (activity!!.isOnline()) {
checkVersion()
} else {
activity!!.toast(MR.strings.no_network_connection)
}
} }
} },
} )
preference {
key = "pref_version"
titleMRes = MR.strings.version
summary = if (BuildConfig.DEBUG || BuildConfig.NIGHTLY) {
"r" + BuildConfig.COMMIT_COUNT
} else {
BuildConfig.VERSION_NAME
}
onClick {
activity?.let {
val deviceInfo = CrashLogUtil(it.localeContext).getDebugInfo()
val clipboard = it.getSystemService<ClipboardManager>()!!
val appInfo = it.getString(MR.strings.app_info)
clipboard.setPrimaryClip(ClipData.newPlainText(appInfo, deviceInfo))
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
view?.snack(context.getString(MR.strings._copied_to_clipboard, appInfo))
}
}
}
}
preference {
key = "pref_build_time"
titleMRes = MR.strings.build_time
summary = getFormattedBuildTime(dateFormat)
}
preferenceCategory {
preference {
key = "pref_oss"
titleMRes = MR.strings.open_source_licenses
onClick {
router.pushController(AboutLicenseController().withFadeTransaction())
}
}
}
add(AboutLinksPreference(context))
}
/**
* Checks version and shows a user prompt if an update is available.
*/
private fun checkVersion() {
val activity = activity ?: return
activity.toast(MR.strings.searching_for_updates)
viewScope.launch {
val result = try {
updateChecker.checkForUpdate(activity, true)
} catch (error: Exception) {
withContext(Dispatchers.Main) {
activity.toast(error.message)
Logger.e(error) { "Couldn't check new update" }
}
}
when (result) {
is AppUpdateResult.NewUpdate -> {
val body = result.release.info
val url = result.release.downloadLink
val isBeta = result.release.preRelease == true
// Create confirmation window
withContext(Dispatchers.Main) {
AppUpdateNotifier.releasePageUrl = result.release.releaseLink
NewUpdateDialogController(body, url, isBeta).showDialog(router)
}
}
is AppUpdateResult.NoNewUpdate -> {
withContext(Dispatchers.Main) {
activity.toast(MR.strings.no_new_updates_available)
}
}
}
}
} }
class NewUpdateDialogController(bundle: Bundle? = null) : DialogController(bundle) { class NewUpdateDialogController(bundle: Bundle? = null) : DialogController(bundle) {
@ -220,19 +100,4 @@ class AboutController : SettingsLegacyController() {
const val IS_BETA = "NewUpdateDialogController.is_beta" const val IS_BETA = "NewUpdateDialogController.is_beta"
} }
} }
companion object {
fun getFormattedBuildTime(dateFormat: DateFormat): String {
try {
val inputDf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.getDefault())
inputDf.timeZone = TimeZone.getTimeZone("UTC")
val buildTime =
inputDf.parse(BuildConfig.BUILD_TIME) ?: return BuildConfig.BUILD_TIME
return buildTime.toTimestampString(dateFormat)
} catch (e: ParseException) {
return BuildConfig.BUILD_TIME
}
}
}
} }

View file

@ -1,27 +0,0 @@
package eu.kanade.tachiyomi.ui.more
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import cafe.adriel.voyager.core.stack.StackEvent
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.transitions.ScreenTransition
import eu.kanade.tachiyomi.ui.base.controller.BaseComposeController
import eu.kanade.tachiyomi.util.compose.LocalBackPress
import soup.compose.material.motion.animation.materialSharedAxisZ
class AboutLicenseController : BaseComposeController() {
@Composable
override fun ScreenContent() {
Navigator(
screen = AboutLicenseScreen(),
content = {
CompositionLocalProvider(LocalBackPress provides router::handleBack) {
ScreenTransition(
navigator = it,
transition = { materialSharedAxisZ(forward = it.lastEvent != StackEvent.Pop) },
)
}
},
)
}
}

View file

@ -1,46 +0,0 @@
package eu.kanade.tachiyomi.ui.more
import android.content.Context
import android.util.AttributeSet
import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.view.compatToolTipText
class AboutLinksPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
Preference(context, attrs) {
init {
layoutResource = R.layout.pref_about_links
isSelectable = false
}
override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)
/*
(holder.itemView as LinearLayout).apply {
checkHeightThen {
val childCount = (this.getChildAt(0) as ViewGroup).childCount
val childCount2 = (this.getChildAt(1) as ViewGroup).childCount
val fullCount = childCount + childCount2
orientation =
if (width >= (56 * fullCount).dpToPx) LinearLayout.HORIZONTAL else LinearLayout.VERTICAL
}
}
*/
holder.findViewById(R.id.btn_website).apply {
compatToolTipText = (contentDescription.toString())
setOnClickListener { context.openInBrowser("https://mihon.app") }
}
holder.findViewById(R.id.btn_discord).apply {
compatToolTipText = (contentDescription.toString())
setOnClickListener { context.openInBrowser("https://discord.gg/mihon") }
}
holder.findViewById(R.id.btn_github).apply {
compatToolTipText = (contentDescription.toString())
setOnClickListener { context.openInBrowser("https://github.com/null2264/yokai") }
}
}
}

View file

@ -4,15 +4,15 @@ import android.os.Build
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import androidx.webkit.WebViewCompat import androidx.webkit.WebViewCompat
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.ui.more.AboutController
import eu.kanade.tachiyomi.ui.setting.SettingsLegacyController import eu.kanade.tachiyomi.ui.setting.SettingsLegacyController
import eu.kanade.tachiyomi.ui.setting.onClick import eu.kanade.tachiyomi.ui.setting.onClick
import eu.kanade.tachiyomi.ui.setting.preference import eu.kanade.tachiyomi.ui.setting.preference
import eu.kanade.tachiyomi.ui.setting.preferenceCategory import eu.kanade.tachiyomi.ui.setting.preferenceCategory
import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.view.withFadeTransaction import eu.kanade.tachiyomi.util.view.withFadeTransaction
import yokai.i18n.MR
import java.text.DateFormat import java.text.DateFormat
import yokai.i18n.MR
import yokai.presentation.settings.screen.about.getFormattedBuildTime
class DebugController : SettingsLegacyController() { class DebugController : SettingsLegacyController() {
@ -49,7 +49,7 @@ class DebugController : SettingsLegacyController() {
preference { preference {
key = "pref_build_time" key = "pref_build_time"
title = "Build Time" title = "Build Time"
summary = AboutController.getFormattedBuildTime(dateFormat) summary = getFormattedBuildTime(dateFormat)
} }
preference { preference {
key = "pref_webview_version" key = "pref_webview_version"

View file

@ -69,7 +69,8 @@ import kotlinx.coroutines.launch
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import yokai.domain.manga.interactor.GetManga import yokai.domain.manga.interactor.GetManga
import yokai.i18n.MR import yokai.i18n.MR
import yokai.presentation.component.icons.LocalSource import yokai.presentation.core.icons.CustomIcons
import yokai.presentation.core.icons.LocalSource
import yokai.util.lang.getString import yokai.util.lang.getString
/** /**
@ -610,7 +611,7 @@ open class BrowseSourceController(bundle: Bundle) :
if (presenter.source is HttpSource) { if (presenter.source is HttpSource) {
Icons.Filled.ExploreOff Icons.Filled.ExploreOff
} else { } else {
Icons.Filled.LocalSource CustomIcons.LocalSource
}, },
message, message,
actions, actions,

View file

@ -23,7 +23,6 @@ import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.dp
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import yokai.i18n.MR import yokai.i18n.MR
@ -35,14 +34,16 @@ fun YokaiScaffold(
onNavigationIconClicked: () -> Unit, onNavigationIconClicked: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
title: String = "", title: String = "",
scrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(state = rememberTopAppBarState()), scrollBehavior: TopAppBarScrollBehavior? = null,
fab: @Composable () -> Unit = {}, fab: @Composable () -> Unit = {},
navigationIcon: ImageVector = Icons.AutoMirrored.Filled.ArrowBack, navigationIcon: ImageVector = Icons.AutoMirrored.Filled.ArrowBack,
navigationIconLabel: String = stringResource(MR.strings.back), navigationIconLabel: String = stringResource(MR.strings.back),
actions: @Composable RowScope.() -> Unit = {}, actions: @Composable RowScope.() -> Unit = {},
appBarType: AppBarType = AppBarType.LARGE, appBarType: AppBarType = AppBarType.LARGE,
snackbarHost: @Composable () -> Unit = {},
content: @Composable (PaddingValues) -> Unit, content: @Composable (PaddingValues) -> Unit,
) { ) {
val scrollBehaviorOrDefault = scrollBehavior ?: TopAppBarDefaults.enterAlwaysScrollBehavior(state = rememberTopAppBarState())
val view = LocalView.current val view = LocalView.current
val useDarkIcons = MaterialTheme.colorScheme.surface.luminance() > .5 val useDarkIcons = MaterialTheme.colorScheme.surface.luminance() > .5
val (color, scrolledColor) = getTopAppBarColor(title) val (color, scrolledColor) = getTopAppBarColor(title)
@ -56,7 +57,7 @@ fun YokaiScaffold(
} }
Scaffold( Scaffold(
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), modifier = modifier.nestedScroll(scrollBehaviorOrDefault.nestedScrollConnection),
floatingActionButton = fab, floatingActionButton = fab,
topBar = { topBar = {
when (appBarType) { when (appBarType) {
@ -76,7 +77,7 @@ fun YokaiScaffold(
buttonClicked = onNavigationIconClicked, buttonClicked = onNavigationIconClicked,
) )
}, },
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehaviorOrDefault,
actions = actions, actions = actions,
) )
AppBarType.LARGE -> ExpandedAppBar( AppBarType.LARGE -> ExpandedAppBar(
@ -95,11 +96,12 @@ fun YokaiScaffold(
buttonClicked = onNavigationIconClicked, buttonClicked = onNavigationIconClicked,
) )
}, },
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehaviorOrDefault,
actions = actions, actions = actions,
) )
} }
}, },
snackbarHost = snackbarHost,
content = content, content = content,
) )
} }

View file

@ -1,39 +0,0 @@
package yokai.presentation.component.icons
import androidx.compose.material.icons.Icons
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
private var _localSource: ImageVector? = null
val Icons.Filled.LocalSource: ImageVector get() {
if (_localSource != null) return _localSource!!
_localSource = ImageVector.Builder(
name = "localSource",
defaultWidth = 24.0.dp,
defaultHeight = 24.0.dp,
viewportWidth = 24.0f,
viewportHeight = 24.0f,
).apply {
path(fill = SolidColor(Color.Black)) {
moveTo(12f, 11.55f)
curveTo(9.64f, 9.35f, 6.48f, 8f, 3f, 8f)
verticalLineToRelative(11f)
curveToRelative(3.48f, 0f, 6.64f, 1.35f, 9f, 3.55f)
curveToRelative(2.36f, -2.19f, 5.52f, -3.55f, 9f, -3.55f)
verticalLineTo(8f)
curveToRelative(-3.48f, 0f, -6.64f, 1.35f, -9f, 3.55f)
close()
moveTo(12f, 8f)
curveToRelative(1.66f, 0f, 3f, -1.34f, 3f, -3f)
reflectiveCurveToRelative(-1.34f, -3f, -3f, -3f)
reflectiveCurveToRelative(-3f, 1.34f, -3f, 3f)
reflectiveCurveToRelative(1.34f, 3f, 3f, 3f)
close()
}
}.build()
return _localSource!!
}

View file

@ -7,6 +7,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@ -35,11 +36,12 @@ fun SettingsScaffold(
title: String, title: String,
appBarType: AppBarType? = null, appBarType: AppBarType? = null,
appBarActions: @Composable RowScope.() -> Unit = {}, appBarActions: @Composable RowScope.() -> Unit = {},
itemsProvider: @Composable () -> List<Preference>, appBarScrollBehavior: TopAppBarScrollBehavior? = null,
snackbarHost: @Composable () -> Unit = {},
content: @Composable (PaddingValues) -> Unit,
) { ) {
val preferences: PreferencesHelper by injectLazy() val preferences: PreferencesHelper by injectLazy()
val useLargeAppBar by preferences.useLargeToolbar().collectAsState() val useLargeAppBar by preferences.useLargeToolbar().collectAsState()
val listState = rememberLazyListState()
val onBackPress = LocalBackPress.currentOrThrow val onBackPress = LocalBackPress.currentOrThrow
val alertDialog = LocalAlertDialog.currentOrThrow val alertDialog = LocalAlertDialog.currentOrThrow
@ -48,14 +50,34 @@ fun SettingsScaffold(
title = title, title = title,
appBarType = appBarType ?: if (useLargeAppBar) AppBarType.LARGE else AppBarType.SMALL, appBarType = appBarType ?: if (useLargeAppBar) AppBarType.LARGE else AppBarType.SMALL,
actions = appBarActions, actions = appBarActions,
scrollBehavior = enterAlwaysCollapsedScrollBehavior( scrollBehavior = appBarScrollBehavior,
snackbarHost = snackbarHost,
) { innerPadding ->
alertDialog.content?.let { it() }
content(innerPadding)
}
}
@Composable
fun SettingsScaffold(
title: String,
appBarType: AppBarType? = null,
appBarActions: @Composable RowScope.() -> Unit = {},
itemsProvider: @Composable () -> List<Preference>,
) {
val listState = rememberLazyListState()
SettingsScaffold(
title = title,
appBarType = appBarType,
appBarActions = appBarActions,
appBarScrollBehavior = enterAlwaysCollapsedScrollBehavior(
state = rememberTopAppBarState(), state = rememberTopAppBarState(),
canScroll = { listState.canScrollForward || listState.canScrollBackward }, canScroll = { listState.canScrollForward || listState.canScrollBackward },
isAtTop = { listState.firstVisibleItemIndex == 0 && listState.firstVisibleItemScrollOffset == 0 }, isAtTop = { listState.firstVisibleItemIndex == 0 && listState.firstVisibleItemScrollOffset == 0 },
), ),
) { innerPadding -> ) { innerPadding ->
alertDialog.content?.let { it() }
PreferenceScreen( PreferenceScreen(
items = itemsProvider(), items = itemsProvider(),
listState = listState, listState = listState,

View file

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.more package yokai.presentation.settings.screen.about
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding

View file

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.more package yokai.presentation.settings.screen.about
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults

View file

@ -0,0 +1,239 @@
package yokai.presentation.settings.screen.about
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Build
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Public
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.unit.dp
import androidx.core.content.getSystemService
import cafe.adriel.voyager.navigator.LocalNavigator
import co.touchlab.kermit.Logger
import dev.icerock.moko.resources.compose.stringResource
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.core.storage.preference.asDateFormat
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.updater.AppUpdateChecker
import eu.kanade.tachiyomi.data.updater.AppUpdateNotifier
import eu.kanade.tachiyomi.data.updater.AppUpdateResult
import eu.kanade.tachiyomi.data.updater.RELEASE_URL
import eu.kanade.tachiyomi.util.CrashLogUtil
import eu.kanade.tachiyomi.util.compose.currentOrThrow
import eu.kanade.tachiyomi.util.lang.toTimestampString
import eu.kanade.tachiyomi.util.system.isOnline
import eu.kanade.tachiyomi.util.system.localeContext
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.system.withUIContext
import java.text.DateFormat
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
import kotlinx.coroutines.launch
import uy.kohesive.injekt.injectLazy
import yokai.i18n.MR
import yokai.presentation.component.preference.widget.TextPreferenceWidget
import yokai.presentation.core.components.LinkIcon
import yokai.presentation.core.enterAlwaysCollapsedScrollBehavior
import yokai.presentation.core.icons.CustomIcons
import yokai.presentation.core.icons.Discord
import yokai.presentation.core.icons.GitHub
import yokai.presentation.settings.SettingsScaffold
import yokai.util.Screen
import yokai.util.lang.getString
class AboutScreen(private val showNewUpdateDialog: (String, String, Boolean?) -> Unit) : Screen() {
@Composable
override fun Content() {
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val uriHandler = LocalUriHandler.current
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
val listState = rememberLazyListState()
val preferences: PreferencesHelper by injectLazy()
val dateFormat by lazy { preferences.dateFormatRaw().get().asDateFormat() }
SettingsScaffold(
title = stringResource(MR.strings.about),
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
},
appBarScrollBehavior = enterAlwaysCollapsedScrollBehavior(
state = rememberTopAppBarState(),
canScroll = { listState.canScrollForward || listState.canScrollBackward },
isAtTop = { listState.firstVisibleItemIndex == 0 && listState.firstVisibleItemScrollOffset == 0 },
),
content = { contentPadding ->
LazyColumn(
contentPadding = contentPadding,
state = listState,
) {
item {
TextPreferenceWidget(
title = stringResource(MR.strings.whats_new_this_release),
onPreferenceClick = {
uriHandler.openUri(if (BuildConfig.DEBUG) SOURCE_URL else RELEASE_URL)
},
)
}
if (BuildConfig.INCLUDE_UPDATER) {
item {
TextPreferenceWidget(
title = stringResource(MR.strings.check_for_updates),
onPreferenceClick = {
if (context.isOnline()) {
scope.launch {
context.checkVersion()
}
} else {
context.toast(MR.strings.no_network_connection)
}
},
)
}
}
item {
TextPreferenceWidget(
title = stringResource(MR.strings.version),
subtitle = getVersionName(),
onPreferenceClick = {
val deviceInfo = CrashLogUtil(context.localeContext).getDebugInfo()
val clipboard = context.getSystemService<ClipboardManager>()!!
val appInfo = context.getString(MR.strings.app_info)
clipboard.setPrimaryClip(ClipData.newPlainText(appInfo, deviceInfo))
scope.launch {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
snackbarHostState.showSnackbar(
message = context.getString(MR.strings._copied_to_clipboard, appInfo),
)
}
}
},
)
}
item {
TextPreferenceWidget(
title = stringResource(MR.strings.version),
subtitle = getFormattedBuildTime(dateFormat),
)
}
item {
Column(modifier = Modifier.fillMaxWidth()) {
HorizontalDivider()
TextPreferenceWidget(
title = stringResource(MR.strings.open_source_licenses),
onPreferenceClick = { navigator.push(AboutLicenseScreen()) }
)
}
}
item {
FlowRow(
modifier =
Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
horizontalArrangement = Arrangement.Center,
) {
LinkIcon(
label = "Website",
icon = Icons.Outlined.Public,
url = "https://mihon.app",
)
LinkIcon(
label = "Discord",
icon = CustomIcons.Discord,
url = "https://discord.gg/mihon",
)
LinkIcon(
label = "GitHub",
icon = CustomIcons.GitHub,
url = "https://github.com/null2264/yokai",
)
}
}
}
},
)
}
private fun getVersionName(): String = when {
BuildConfig.DEBUG -> "Debug ${BuildConfig.COMMIT_SHA}"
BuildConfig.NIGHTLY -> "Nightly ${BuildConfig.COMMIT_COUNT} (${BuildConfig.COMMIT_SHA})"
else -> "Release ${BuildConfig.VERSION_NAME}"
}
private suspend fun Context.checkVersion() {
val updateChecker = AppUpdateChecker()
withUIContext { toast(MR.strings.searching_for_updates) }
val result = try {
updateChecker.checkForUpdate(this, true)
} catch (error: Exception) {
withUIContext {
toast(error.message)
Logger.e(error) { "Couldn't check new update" }
}
}
when (result) {
is AppUpdateResult.NewUpdate -> {
val body = result.release.info
val url = result.release.downloadLink
val isBeta = result.release.preRelease == true
// Create confirmation window
withUIContext {
AppUpdateNotifier.releasePageUrl = result.release.releaseLink
showNewUpdateDialog(body, url, isBeta)
}
}
is AppUpdateResult.NoNewUpdate -> {
withUIContext {
toast(MR.strings.no_new_updates_available)
}
}
}
}
}
fun getFormattedBuildTime(dateFormat: DateFormat): String {
try {
val inputDf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.getDefault())
inputDf.timeZone = TimeZone.getTimeZone("UTC")
val buildTime =
inputDf.parse(BuildConfig.BUILD_TIME) ?: return BuildConfig.BUILD_TIME
return buildTime.toTimestampString(dateFormat)
} catch (e: ParseException) {
return BuildConfig.BUILD_TIME
}
}
private const val SOURCE_URL = "https://github.com/null2264/yokai/commits/master"

View file

@ -0,0 +1,31 @@
package yokai.presentation.core.components
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.unit.dp
@Composable
fun LinkIcon(
label: String,
icon: ImageVector,
url: String,
modifier: Modifier = Modifier,
) {
val uriHandler = LocalUriHandler.current
IconButton(
modifier = modifier.padding(4.dp),
onClick = { uriHandler.openUri(url) },
) {
Icon(
imageVector = icon,
tint = MaterialTheme.colorScheme.primary,
contentDescription = label,
)
}
}

View file

@ -0,0 +1,3 @@
package yokai.presentation.core.icons
object CustomIcons

View file

@ -0,0 +1,86 @@
package yokai.presentation.core.icons
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathFillType.Companion.NonZero
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.StrokeCap.Companion.Butt
import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.ImageVector.Builder
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
@Suppress("UnusedReceiverParameter", "BooleanLiteralArgument")
val CustomIcons.Discord: ImageVector
get() {
if (_discord != null) {
return _discord!!
}
_discord = Builder(
name = "Discord",
defaultWidth = 24.0.dp,
defaultHeight = 24.0.dp,
viewportWidth = 24.0f,
viewportHeight = 24.0f,
).apply {
path(
fill = SolidColor(Color(0xFF000000)),
stroke = null,
strokeLineWidth = 0.0f,
strokeLineCap = Butt,
strokeLineJoin = Miter,
strokeLineMiter = 4.0f,
pathFillType = NonZero,
) {
moveTo(20.317f, 4.3698f)
arcToRelative(19.7913f, 19.7913f, 0.0f, false, false, -4.8851f, -1.5152f)
arcToRelative(0.0741f, 0.0741f, 0.0f, false, false, -0.0785f, 0.0371f)
curveToRelative(-0.211f, 0.3753f, -0.4447f, 0.8648f, -0.6083f, 1.2495f)
curveToRelative(-1.8447f, -0.2762f, -3.68f, -0.2762f, -5.4868f, 0.0f)
curveToRelative(-0.1636f, -0.3933f, -0.4058f, -0.8742f, -0.6177f, -1.2495f)
arcToRelative(0.077f, 0.077f, 0.0f, false, false, -0.0785f, -0.037f)
arcToRelative(19.7363f, 19.7363f, 0.0f, false, false, -4.8852f, 1.515f)
arcToRelative(0.0699f, 0.0699f, 0.0f, false, false, -0.0321f, 0.0277f)
curveTo(0.5334f, 9.0458f, -0.319f, 13.5799f, 0.0992f, 18.0578f)
arcToRelative(0.0824f, 0.0824f, 0.0f, false, false, 0.0312f, 0.0561f)
curveToRelative(2.0528f, 1.5076f, 4.0413f, 2.4228f, 5.9929f, 3.0294f)
arcToRelative(0.0777f, 0.0777f, 0.0f, false, false, 0.0842f, -0.0276f)
curveToRelative(0.4616f, -0.6304f, 0.8731f, -1.2952f, 1.226f, -1.9942f)
arcToRelative(0.076f, 0.076f, 0.0f, false, false, -0.0416f, -0.1057f)
curveToRelative(-0.6528f, -0.2476f, -1.2743f, -0.5495f, -1.8722f, -0.8923f)
arcToRelative(0.077f, 0.077f, 0.0f, false, true, -0.0076f, -0.1277f)
curveToRelative(0.1258f, -0.0943f, 0.2517f, -0.1923f, 0.3718f, -0.2914f)
arcToRelative(0.0743f, 0.0743f, 0.0f, false, true, 0.0776f, -0.0105f)
curveToRelative(3.9278f, 1.7933f, 8.18f, 1.7933f, 12.0614f, 0.0f)
arcToRelative(0.0739f, 0.0739f, 0.0f, false, true, 0.0785f, 0.0095f)
curveToRelative(0.1202f, 0.099f, 0.246f, 0.1981f, 0.3728f, 0.2924f)
arcToRelative(0.077f, 0.077f, 0.0f, false, true, -0.0066f, 0.1276f)
arcToRelative(12.2986f, 12.2986f, 0.0f, false, true, -1.873f, 0.8914f)
arcToRelative(0.0766f, 0.0766f, 0.0f, false, false, -0.0407f, 0.1067f)
curveToRelative(0.3604f, 0.698f, 0.7719f, 1.3628f, 1.225f, 1.9932f)
arcToRelative(0.076f, 0.076f, 0.0f, false, false, 0.0842f, 0.0286f)
curveToRelative(1.961f, -0.6067f, 3.9495f, -1.5219f, 6.0023f, -3.0294f)
arcToRelative(0.077f, 0.077f, 0.0f, false, false, 0.0313f, -0.0552f)
curveToRelative(0.5004f, -5.177f, -0.8382f, -9.6739f, -3.5485f, -13.6604f)
arcToRelative(0.061f, 0.061f, 0.0f, false, false, -0.0312f, -0.0286f)
close()
moveTo(8.02f, 15.3312f)
curveToRelative(-1.1825f, 0.0f, -2.1569f, -1.0857f, -2.1569f, -2.419f)
curveToRelative(0.0f, -1.3332f, 0.9555f, -2.4189f, 2.157f, -2.4189f)
curveToRelative(1.2108f, 0.0f, 2.1757f, 1.0952f, 2.1568f, 2.419f)
curveToRelative(0.0f, 1.3332f, -0.9555f, 2.4189f, -2.1569f, 2.4189f)
close()
moveTo(15.9948f, 15.3312f)
curveToRelative(-1.1825f, 0.0f, -2.1569f, -1.0857f, -2.1569f, -2.419f)
curveToRelative(0.0f, -1.3332f, 0.9554f, -2.4189f, 2.1569f, -2.4189f)
curveToRelative(1.2108f, 0.0f, 2.1757f, 1.0952f, 2.1568f, 2.419f)
curveToRelative(0.0f, 1.3332f, -0.946f, 2.4189f, -2.1568f, 2.4189f)
close()
}
}
.build()
return _discord!!
}
@Suppress("ObjectPropertyName")
private var _discord: ImageVector? = null

View file

@ -0,0 +1,68 @@
package yokai.presentation.core.icons
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathFillType.Companion.NonZero
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.StrokeCap.Companion.Butt
import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.ImageVector.Builder
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
@Suppress("UnusedReceiverParameter")
val CustomIcons.GitHub: ImageVector
get() {
if (_github != null) {
return _github!!
}
_github = Builder(
name = "GitHub",
defaultWidth = 24.0.dp,
defaultHeight = 24.0.dp,
viewportWidth = 24.0f,
viewportHeight = 24.0f,
).apply {
path(
fill = SolidColor(Color(0xFF000000)),
stroke = null,
strokeLineWidth = 0.0f,
strokeLineCap = Butt,
strokeLineJoin = Miter,
strokeLineMiter = 4.0f,
pathFillType = NonZero,
) {
moveTo(12.0f, 0.297f)
curveToRelative(-6.63f, 0.0f, -12.0f, 5.373f, -12.0f, 12.0f)
curveToRelative(0.0f, 5.303f, 3.438f, 9.8f, 8.205f, 11.385f)
curveToRelative(0.6f, 0.113f, 0.82f, -0.258f, 0.82f, -0.577f)
curveToRelative(0.0f, -0.285f, -0.01f, -1.04f, -0.015f, -2.04f)
curveToRelative(-3.338f, 0.724f, -4.042f, -1.61f, -4.042f, -1.61f)
curveTo(4.422f, 18.07f, 3.633f, 17.7f, 3.633f, 17.7f)
curveToRelative(-1.087f, -0.744f, 0.084f, -0.729f, 0.084f, -0.729f)
curveToRelative(1.205f, 0.084f, 1.838f, 1.236f, 1.838f, 1.236f)
curveToRelative(1.07f, 1.835f, 2.809f, 1.305f, 3.495f, 0.998f)
curveToRelative(0.108f, -0.776f, 0.417f, -1.305f, 0.76f, -1.605f)
curveToRelative(-2.665f, -0.3f, -5.466f, -1.332f, -5.466f, -5.93f)
curveToRelative(0.0f, -1.31f, 0.465f, -2.38f, 1.235f, -3.22f)
curveToRelative(-0.135f, -0.303f, -0.54f, -1.523f, 0.105f, -3.176f)
curveToRelative(0.0f, 0.0f, 1.005f, -0.322f, 3.3f, 1.23f)
curveToRelative(0.96f, -0.267f, 1.98f, -0.399f, 3.0f, -0.405f)
curveToRelative(1.02f, 0.006f, 2.04f, 0.138f, 3.0f, 0.405f)
curveToRelative(2.28f, -1.552f, 3.285f, -1.23f, 3.285f, -1.23f)
curveToRelative(0.645f, 1.653f, 0.24f, 2.873f, 0.12f, 3.176f)
curveToRelative(0.765f, 0.84f, 1.23f, 1.91f, 1.23f, 3.22f)
curveToRelative(0.0f, 4.61f, -2.805f, 5.625f, -5.475f, 5.92f)
curveToRelative(0.42f, 0.36f, 0.81f, 1.096f, 0.81f, 2.22f)
curveToRelative(0.0f, 1.606f, -0.015f, 2.896f, -0.015f, 3.286f)
curveToRelative(0.0f, 0.315f, 0.21f, 0.69f, 0.825f, 0.57f)
curveTo(20.565f, 22.092f, 24.0f, 17.592f, 24.0f, 12.297f)
curveToRelative(0.0f, -6.627f, -5.373f, -12.0f, -12.0f, -12.0f)
}
}
.build()
return _github!!
}
@Suppress("ObjectPropertyName")
private var _github: ImageVector? = null

View file

@ -0,0 +1,56 @@
package yokai.presentation.core.icons
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathFillType.Companion.NonZero
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.StrokeCap.Companion.Butt
import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.ImageVector.Builder
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
@Suppress("UnusedReceiverParameter")
val CustomIcons.LocalSource: ImageVector
get() {
if (_localSource != null) {
return _localSource!!
}
_localSource = Builder(
name = "localSource",
defaultWidth = 24.0.dp,
defaultHeight = 24.0.dp,
viewportWidth = 24.0f,
viewportHeight = 24.0f,
).apply {
path(
fill = SolidColor(Color(0xFF000000)),
stroke = null,
strokeLineWidth = 0.0f,
strokeLineCap = Butt,
strokeLineJoin = Miter,
strokeLineMiter = 4.0f,
pathFillType = NonZero,
) {
moveTo(12f, 11.55f)
curveTo(9.64f, 9.35f, 6.48f, 8f, 3f, 8f)
verticalLineToRelative(11f)
curveToRelative(3.48f, 0f, 6.64f, 1.35f, 9f, 3.55f)
curveToRelative(2.36f, -2.19f, 5.52f, -3.55f, 9f, -3.55f)
verticalLineTo(8f)
curveToRelative(-3.48f, 0f, -6.64f, 1.35f, -9f, 3.55f)
close()
moveTo(12f, 8f)
curveToRelative(1.66f, 0f, 3f, -1.34f, 3f, -3f)
reflectiveCurveToRelative(-1.34f, -3f, -3f, -3f)
reflectiveCurveToRelative(-3f, 1.34f, -3f, 3f)
reflectiveCurveToRelative(1.34f, 3f, 3f, 3f)
close()
}
}
.build()
return _localSource!!
}
@Suppress("ObjectPropertyName")
private var _localSource: ImageVector? = null