diff --git a/CHANGELOG.md b/CHANGELOG.md index f1dacc1ab3..0f48dcf25f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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] diff --git a/app/src/main/java/eu/kanade/tachiyomi/core/storage/preference/PreferenceExtension.kt b/app/src/main/java/eu/kanade/tachiyomi/core/storage/preference/PreferenceExtension.kt index be0b8e9773..806b80ea22 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/core/storage/preference/PreferenceExtension.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/core/storage/preference/PreferenceExtension.kt @@ -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 Preference.collectAsState(): State { 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()) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index ec29bc9262..5d63f59b83 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -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", "") diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutController.kt index 5fd462890a..4d1f9a63c8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutController.kt @@ -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()!! - 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 - } - } - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutLicenseController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutLicenseController.kt deleted file mode 100644 index 6a2ea78a8e..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutLicenseController.kt +++ /dev/null @@ -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) }, - ) - } - }, - ) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutLinksPreference.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutLinksPreference.kt deleted file mode 100644 index df08c5852d..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutLinksPreference.kt +++ /dev/null @@ -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") } - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/debug/DebugController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/debug/DebugController.kt index ea69af4080..503b715a41 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/debug/DebugController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/debug/DebugController.kt @@ -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" diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt index 28b847bfca..e27e2eb2b7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt @@ -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, diff --git a/app/src/main/java/yokai/presentation/Scaffold.kt b/app/src/main/java/yokai/presentation/Scaffold.kt index c61e0daffc..7122df5f38 100644 --- a/app/src/main/java/yokai/presentation/Scaffold.kt +++ b/app/src/main/java/yokai/presentation/Scaffold.kt @@ -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, ) } diff --git a/app/src/main/java/yokai/presentation/component/icons/LocalSource.kt b/app/src/main/java/yokai/presentation/component/icons/LocalSource.kt deleted file mode 100644 index 8d579b2cc1..0000000000 --- a/app/src/main/java/yokai/presentation/component/icons/LocalSource.kt +++ /dev/null @@ -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!! -} diff --git a/app/src/main/java/yokai/presentation/settings/SettingsCommonWidget.kt b/app/src/main/java/yokai/presentation/settings/SettingsCommonWidget.kt index cff9066e2a..7fc0fdb185 100644 --- a/app/src/main/java/yokai/presentation/settings/SettingsCommonWidget.kt +++ b/app/src/main/java/yokai/presentation/settings/SettingsCommonWidget.kt @@ -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, + 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, +) { + 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, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutLibraryLicenseScreen.kt b/app/src/main/java/yokai/presentation/settings/screen/about/AboutLibraryLicenseScreen.kt similarity index 98% rename from app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutLibraryLicenseScreen.kt rename to app/src/main/java/yokai/presentation/settings/screen/about/AboutLibraryLicenseScreen.kt index 11b2e7c66d..7ed11d10d8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutLibraryLicenseScreen.kt +++ b/app/src/main/java/yokai/presentation/settings/screen/about/AboutLibraryLicenseScreen.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutLicenseScreen.kt b/app/src/main/java/yokai/presentation/settings/screen/about/AboutLicenseScreen.kt similarity index 97% rename from app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutLicenseScreen.kt rename to app/src/main/java/yokai/presentation/settings/screen/about/AboutLicenseScreen.kt index c98bc48cfa..6d4ca8be2a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutLicenseScreen.kt +++ b/app/src/main/java/yokai/presentation/settings/screen/about/AboutLicenseScreen.kt @@ -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 diff --git a/app/src/main/java/yokai/presentation/settings/screen/about/AboutScreen.kt b/app/src/main/java/yokai/presentation/settings/screen/about/AboutScreen.kt new file mode 100644 index 0000000000..d70460d156 --- /dev/null +++ b/app/src/main/java/yokai/presentation/settings/screen/about/AboutScreen.kt @@ -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()!! + 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" diff --git a/presentation/core/src/main/java/yokai/presentation/core/components/LinkIcon.kt b/presentation/core/src/main/java/yokai/presentation/core/components/LinkIcon.kt new file mode 100644 index 0000000000..03d02b421d --- /dev/null +++ b/presentation/core/src/main/java/yokai/presentation/core/components/LinkIcon.kt @@ -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, + ) + } +} diff --git a/presentation/core/src/main/java/yokai/presentation/core/icons/CustomIcons.kt b/presentation/core/src/main/java/yokai/presentation/core/icons/CustomIcons.kt new file mode 100644 index 0000000000..6b6ce894a2 --- /dev/null +++ b/presentation/core/src/main/java/yokai/presentation/core/icons/CustomIcons.kt @@ -0,0 +1,3 @@ +package yokai.presentation.core.icons + +object CustomIcons diff --git a/presentation/core/src/main/java/yokai/presentation/core/icons/Discord.kt b/presentation/core/src/main/java/yokai/presentation/core/icons/Discord.kt new file mode 100644 index 0000000000..5eb4d368ee --- /dev/null +++ b/presentation/core/src/main/java/yokai/presentation/core/icons/Discord.kt @@ -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 diff --git a/presentation/core/src/main/java/yokai/presentation/core/icons/GitHub.kt b/presentation/core/src/main/java/yokai/presentation/core/icons/GitHub.kt new file mode 100644 index 0000000000..5115f5d0ff --- /dev/null +++ b/presentation/core/src/main/java/yokai/presentation/core/icons/GitHub.kt @@ -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 diff --git a/presentation/core/src/main/java/yokai/presentation/core/icons/LocalSource.kt b/presentation/core/src/main/java/yokai/presentation/core/icons/LocalSource.kt new file mode 100644 index 0000000000..b45c727597 --- /dev/null +++ b/presentation/core/src/main/java/yokai/presentation/core/icons/LocalSource.kt @@ -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