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 Reader ChapterTransition to use Compose (@arkon)
- [Experimental] Add modified version of LargeTopAppBar that mimic J2K's ExpandedAppBarLayout
- Refactor About page to use Compose
## [1.9.7]

View file

@ -5,9 +5,17 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import eu.kanade.tachiyomi.core.preference.Preference
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Locale
@Composable
fun <T> Preference<T>.collectAsState(): State<T> {
val flow = remember(this) { changes() }
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.PreferenceStore
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.domain.manga.models.Manga
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.recents.RecentsPresenter
import eu.kanade.tachiyomi.util.system.Themes
import java.text.DateFormat
import java.util.Locale
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
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.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 dateFormat(format: String = preferenceStore.getString(Keys.dateFormat, "").get()): DateFormat = when (format) {
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
else -> SimpleDateFormat(format, Locale.getDefault())
}
fun dateFormatRaw() = preferenceStore.getString(Keys.dateFormat, "")
@Deprecated("Use dateFormatRaw().get().asDateFormat() instead")
fun dateFormat(format: String = dateFormatRaw().get()): DateFormat = format.asDateFormat()
fun appLanguage() = preferenceStore.getString("app_language", "")

View file

@ -1,173 +1,53 @@
package eu.kanade.tachiyomi.ui.more
import android.app.Dialog
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.view.View
import android.widget.TextView
import androidx.core.content.getSystemService
import androidx.core.net.toUri
import androidx.preference.PreferenceScreen
import co.touchlab.kermit.Logger
import eu.kanade.tachiyomi.BuildConfig
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.data.updater.AppDownloadInstallJob
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.ui.base.controller.BaseComposeController
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.setting.SettingsLegacyController
import eu.kanade.tachiyomi.ui.setting.add
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.compose.LocalAlertDialog
import eu.kanade.tachiyomi.util.compose.LocalBackPress
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.setPositiveButton
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import soup.compose.material.motion.animation.materialSharedAxisZ
import yokai.domain.ComposableAlertDialog
import yokai.i18n.MR
import yokai.util.lang.getString
import java.text.DateFormat
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
import yokai.presentation.settings.screen.about.AboutScreen
import android.R as AR
class AboutController : SettingsLegacyController() {
class AboutController : BaseComposeController() {
/**
* Checks for new releases
*/
private val updateChecker by lazy { AppUpdateChecker() }
private val dateFormat: DateFormat by lazy {
preferences.dateFormat()
}
private val isUpdaterEnabled = BuildConfig.INCLUDE_UPDATER
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleMRes = MR.strings.about
preference {
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)
}
@Composable
override fun ScreenContent() {
Navigator(
screen = AboutScreen { body, url, isBeta ->
NewUpdateDialogController(body, url, isBeta).showDialog(router)
},
content = {
CompositionLocalProvider(
LocalAlertDialog provides ComposableAlertDialog(null),
LocalBackPress provides router::handleBack,
) {
ScreenTransition(
navigator = it,
// FIXME: Mimic J2K's Conductor transition
transition = { materialSharedAxisZ(forward = it.lastEvent != StackEvent.Pop) },
)
}
}
}
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) {
@ -220,19 +100,4 @@ class AboutController : SettingsLegacyController() {
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.webkit.WebViewCompat
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.onClick
import eu.kanade.tachiyomi.ui.setting.preference
import eu.kanade.tachiyomi.ui.setting.preferenceCategory
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.view.withFadeTransaction
import yokai.i18n.MR
import java.text.DateFormat
import yokai.i18n.MR
import yokai.presentation.settings.screen.about.getFormattedBuildTime
class DebugController : SettingsLegacyController() {
@ -49,7 +49,7 @@ class DebugController : SettingsLegacyController() {
preference {
key = "pref_build_time"
title = "Build Time"
summary = AboutController.getFormattedBuildTime(dateFormat)
summary = getFormattedBuildTime(dateFormat)
}
preference {
key = "pref_webview_version"

View file

@ -69,7 +69,8 @@ import kotlinx.coroutines.launch
import uy.kohesive.injekt.injectLazy
import yokai.domain.manga.interactor.GetManga
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
/**
@ -610,7 +611,7 @@ open class BrowseSourceController(bundle: Bundle) :
if (presenter.source is HttpSource) {
Icons.Filled.ExploreOff
} else {
Icons.Filled.LocalSource
CustomIcons.LocalSource
},
message,
actions,

View file

@ -23,7 +23,6 @@ import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.dp
import androidx.core.view.WindowInsetsControllerCompat
import dev.icerock.moko.resources.compose.stringResource
import yokai.i18n.MR
@ -35,14 +34,16 @@ fun YokaiScaffold(
onNavigationIconClicked: () -> Unit,
modifier: Modifier = Modifier,
title: String = "",
scrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(state = rememberTopAppBarState()),
scrollBehavior: TopAppBarScrollBehavior? = null,
fab: @Composable () -> Unit = {},
navigationIcon: ImageVector = Icons.AutoMirrored.Filled.ArrowBack,
navigationIconLabel: String = stringResource(MR.strings.back),
actions: @Composable RowScope.() -> Unit = {},
appBarType: AppBarType = AppBarType.LARGE,
snackbarHost: @Composable () -> Unit = {},
content: @Composable (PaddingValues) -> Unit,
) {
val scrollBehaviorOrDefault = scrollBehavior ?: TopAppBarDefaults.enterAlwaysScrollBehavior(state = rememberTopAppBarState())
val view = LocalView.current
val useDarkIcons = MaterialTheme.colorScheme.surface.luminance() > .5
val (color, scrolledColor) = getTopAppBarColor(title)
@ -56,7 +57,7 @@ fun YokaiScaffold(
}
Scaffold(
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
modifier = modifier.nestedScroll(scrollBehaviorOrDefault.nestedScrollConnection),
floatingActionButton = fab,
topBar = {
when (appBarType) {
@ -76,7 +77,7 @@ fun YokaiScaffold(
buttonClicked = onNavigationIconClicked,
)
},
scrollBehavior = scrollBehavior,
scrollBehavior = scrollBehaviorOrDefault,
actions = actions,
)
AppBarType.LARGE -> ExpandedAppBar(
@ -95,11 +96,12 @@ fun YokaiScaffold(
buttonClicked = onNavigationIconClicked,
)
},
scrollBehavior = scrollBehavior,
scrollBehavior = scrollBehaviorOrDefault,
actions = actions,
)
}
},
snackbarHost = snackbarHost,
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.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -35,11 +36,12 @@ fun SettingsScaffold(
title: String,
appBarType: AppBarType? = null,
appBarActions: @Composable RowScope.() -> Unit = {},
itemsProvider: @Composable () -> List<Preference>,
appBarScrollBehavior: TopAppBarScrollBehavior? = null,
snackbarHost: @Composable () -> Unit = {},
content: @Composable (PaddingValues) -> Unit,
) {
val preferences: PreferencesHelper by injectLazy()
val useLargeAppBar by preferences.useLargeToolbar().collectAsState()
val listState = rememberLazyListState()
val onBackPress = LocalBackPress.currentOrThrow
val alertDialog = LocalAlertDialog.currentOrThrow
@ -48,14 +50,34 @@ fun SettingsScaffold(
title = title,
appBarType = appBarType ?: if (useLargeAppBar) AppBarType.LARGE else AppBarType.SMALL,
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(),
canScroll = { listState.canScrollForward || listState.canScrollBackward },
isAtTop = { listState.firstVisibleItemIndex == 0 && listState.firstVisibleItemScrollOffset == 0 },
),
) { innerPadding ->
alertDialog.content?.let { it() }
PreferenceScreen(
items = itemsProvider(),
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.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.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