diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0d7f497c5c..fafd52e0d2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,20 +34,20 @@ jobs: steps: - name: Checkout - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Dependency Review if: github.event_name == 'pull_request' uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2 - name: Set up JDK - uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 with: java-version: 17 distribution: temurin - name: Set up Gradle - uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0 + uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1 - name: Check code format run: ./gradlew spotlessCheck diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2a3d256b1c..b6ce85aabf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,10 +35,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up JDK - uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 with: java-version: 17 distribution: temurin @@ -53,7 +53,7 @@ jobs: # <-- AM (SYNC_DRIVE) - name: Set up Gradle - uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0 + uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1 - name: Build run: ./gradlew assembleRelease -Penable-updater diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9689bca070..993179eaaa 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -158,12 +158,14 @@ kotlin { "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi", "-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi", "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api", + "-opt-in=androidx.compose.material3.ExperimentalMaterial3ExpressiveApi", "-opt-in=androidx.compose.ui.ExperimentalComposeUiApi", "-opt-in=coil3.annotation.ExperimentalCoilApi", "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", "-opt-in=kotlinx.coroutines.FlowPreview", "-opt-in=kotlinx.coroutines.InternalCoroutinesApi", "-opt-in=kotlinx.serialization.ExperimentalSerializationApi", + "-Xannotation-default-target=param-property", ) } } @@ -286,6 +288,7 @@ dependencies { implementation(libs.compose.grid) implementation(libs.reorderable) implementation(libs.bundles.markdown) + implementation(libs.materialKolor) // Logging implementation(libs.logcat) diff --git a/app/src/main/java/eu/kanade/core/util/FlowUtil.kt b/app/src/main/java/eu/kanade/core/util/FlowUtil.kt new file mode 100644 index 0000000000..ab92eadb5a --- /dev/null +++ b/app/src/main/java/eu/kanade/core/util/FlowUtil.kt @@ -0,0 +1,27 @@ +// AM --> +package eu.kanade.core.util + +import kotlinx.coroutines.flow.Flow + +inline fun combine( + flow: Flow, + flow2: Flow, + flow3: Flow, + flow4: Flow, + flow5: Flow, + flow6: Flow, + crossinline transform: suspend (T1, T2, T3, T4, T5, T6) -> R, +): Flow { + return kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6) { args: Array<*> -> + @Suppress("UNCHECKED_CAST") + transform( + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5, + args[5] as T6, + ) + } +} +// <-- AM diff --git a/app/src/main/java/eu/kanade/presentation/anime/AnimeScreen.kt b/app/src/main/java/eu/kanade/presentation/anime/AnimeScreen.kt index 974c6b87cc..d9f56c80b3 100644 --- a/app/src/main/java/eu/kanade/presentation/anime/AnimeScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/anime/AnimeScreen.kt @@ -1,10 +1,7 @@ package eu.kanade.presentation.anime import androidx.activity.compose.BackHandler -import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column @@ -29,9 +26,11 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material3.Icon +import androidx.compose.material3.SmallExtendedFloatingActionButton import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text +import androidx.compose.material3.animateFloatingActionButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf @@ -92,7 +91,6 @@ import tachiyomi.i18n.aniyomi.AYMR import tachiyomi.presentation.core.components.FastScrollIrregularLazyVerticalGrid import tachiyomi.presentation.core.components.Scroller.EXACT_HEIGHT_KEY_PREFIX import tachiyomi.presentation.core.components.TwoPanelBox -import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton import tachiyomi.presentation.core.components.material.PullRefresh import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.i18n.stringResource @@ -479,27 +477,25 @@ private fun AnimeScreenSmallImpl( val isFABVisible = remember(episodes) { episodes.fastAny { !it.episode.seen } && !isAnySelected } - AnimatedVisibility( - visible = isFABVisible, - enter = fadeIn(), - exit = fadeOut(), - ) { - ExtendedFloatingActionButton( - text = { - val isWatching = remember(state.episodes) { - state.episodes.fastAny { it.episode.seen } - } - Text( - text = stringResource( - if (isWatching) MR.strings.action_resume else MR.strings.action_start, - ), - ) - }, - icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) }, - onClick = onContinueWatching, - expanded = itemListState.shouldExpandFAB(), - ) - } + SmallExtendedFloatingActionButton( + text = { + val isWatching = remember(state.episodes) { + state.episodes.fastAny { it.episode.seen } + } + Text( + text = stringResource( + if (isWatching) MR.strings.action_resume else MR.strings.action_start, + ), + ) + }, + icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) }, + onClick = onContinueWatching, + expanded = itemListState.shouldExpandFAB(), + modifier = Modifier.animateFloatingActionButton( + visible = isFABVisible, + alignment = Alignment.BottomEnd, + ), + ) }, ) { contentPadding -> val topPadding = contentPadding.calculateTopPadding() @@ -864,27 +860,25 @@ fun AnimeScreenLargeImpl( val isFABVisible = remember(episodes) { episodes.fastAny { !it.episode.seen } && !isAnySelected } - AnimatedVisibility( - visible = isFABVisible, - enter = fadeIn(), - exit = fadeOut(), - ) { - ExtendedFloatingActionButton( - text = { - val isWatching = remember(state.episodes) { - state.episodes.fastAny { it.episode.seen } - } - Text( - text = stringResource( - if (isWatching) MR.strings.action_resume else MR.strings.action_start, - ), - ) - }, - icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) }, - onClick = onContinueWatching, - expanded = itemListState.shouldExpandFAB(), - ) - } + SmallExtendedFloatingActionButton( + text = { + val isWatching = remember(state.episodes) { + state.episodes.fastAny { it.episode.seen } + } + Text( + text = stringResource( + if (isWatching) MR.strings.action_resume else MR.strings.action_start, + ), + ) + }, + icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) }, + onClick = onContinueWatching, + expanded = itemListState.shouldExpandFAB(), + modifier = Modifier.animateFloatingActionButton( + visible = isFABVisible, + alignment = Alignment.BottomEnd, + ), + ) }, ) { contentPadding -> PullRefresh( diff --git a/app/src/main/java/eu/kanade/presentation/anime/components/AnimeBottomActionMenu.kt b/app/src/main/java/eu/kanade/presentation/anime/components/AnimeBottomActionMenu.kt index 6f25d6f978..ddc38fdba4 100644 --- a/app/src/main/java/eu/kanade/presentation/anime/components/AnimeBottomActionMenu.kt +++ b/app/src/main/java/eu/kanade/presentation/anime/components/AnimeBottomActionMenu.kt @@ -114,12 +114,11 @@ fun AnimeBottomActionMenu( val confirm = remember { mutableStateListOf(false, false, false, false, false, false, false, false, false, false, false) } - val confirmRange = 0..<11 // <-- AY - var resetJob: Job? = remember { null } + var resetJob by remember { mutableStateOf(null) } val onLongClickItem: (Int) -> Unit = { toConfirmIndex -> haptic.performHapticFeedback(HapticFeedbackType.LongPress) - (confirmRange).forEach { i -> confirm[i] = i == toConfirmIndex } + confirm.indices.forEach { i -> confirm[i] = i == toConfirmIndex } resetJob?.cancel() resetJob = scope.launch { delay(1.seconds) @@ -319,10 +318,10 @@ fun LibraryBottomActionMenu( ) { val haptic = LocalHapticFeedback.current val confirm = remember { mutableStateListOf(false, false, false, false, false, false) } - var resetJob: Job? = remember { null } + var resetJob by remember { mutableStateOf(null) } val onLongClickItem: (Int) -> Unit = { toConfirmIndex -> haptic.performHapticFeedback(HapticFeedbackType.LongPress) - (0..5).forEach { i -> confirm[i] = i == toConfirmIndex } + confirm.indices.forEach { i -> confirm[i] = i == toConfirmIndex } resetJob?.cancel() resetJob = scope.launch { delay(1.seconds) diff --git a/app/src/main/java/eu/kanade/presentation/anime/components/AnimeInfoHeader.kt b/app/src/main/java/eu/kanade/presentation/anime/components/AnimeInfoHeader.kt index 7ad4d27e6f..835b8ee3b0 100644 --- a/app/src/main/java/eu/kanade/presentation/anime/components/AnimeInfoHeader.kt +++ b/app/src/main/java/eu/kanade/presentation/anime/components/AnimeInfoHeader.kt @@ -577,44 +577,47 @@ private fun ColumnScope.AnimeContentInfo( } } -private fun descriptionAnnotator(loadImages: Boolean, linkStyle: SpanStyle) = markdownAnnotator( - annotate = { content, child -> - if (!loadImages && child.type == MarkdownElementTypes.IMAGE) { - val inlineLink = child.findChildOfType(MarkdownElementTypes.INLINE_LINK) +@Composable +private fun descriptionAnnotator(loadImages: Boolean, linkStyle: SpanStyle) = remember(loadImages, linkStyle) { + markdownAnnotator( + annotate = { content, child -> + if (!loadImages && child.type == MarkdownElementTypes.IMAGE) { + val inlineLink = child.findChildOfType(MarkdownElementTypes.INLINE_LINK) - val url = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_DESTINATION) - ?.getUnescapedTextInNode(content) - ?: inlineLink?.findChildOfType(MarkdownElementTypes.AUTOLINK) - ?.findChildOfType(MarkdownTokenTypes.AUTOLINK) + val url = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_DESTINATION) ?.getUnescapedTextInNode(content) - ?: return@markdownAnnotator false + ?: inlineLink?.findChildOfType(MarkdownElementTypes.AUTOLINK) + ?.findChildOfType(MarkdownTokenTypes.AUTOLINK) + ?.getUnescapedTextInNode(content) + ?: return@markdownAnnotator false - val textNode = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TITLE) - ?: inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TEXT) - val altText = textNode?.findChildOfType(MarkdownTokenTypes.TEXT) - ?.getUnescapedTextInNode(content).orEmpty() + val textNode = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TITLE) + ?: inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TEXT) + val altText = textNode?.findChildOfType(MarkdownTokenTypes.TEXT) + ?.getUnescapedTextInNode(content).orEmpty() - withLink(LinkAnnotation.Url(url = url)) { - pushStyle(linkStyle) - appendInlineContent(MARKDOWN_INLINE_IMAGE_TAG) - append(altText) - pop() - } + withLink(LinkAnnotation.Url(url = url)) { + pushStyle(linkStyle) + appendInlineContent(MARKDOWN_INLINE_IMAGE_TAG) + append(altText) + pop() + } - return@markdownAnnotator true - } + return@markdownAnnotator true + } - if (child.type in DISALLOWED_MARKDOWN_TYPES) { - append(content.substring(child.startOffset, child.endOffset)) - return@markdownAnnotator true - } + if (child.type in DISALLOWED_MARKDOWN_TYPES) { + append(content.substring(child.startOffset, child.endOffset)) + return@markdownAnnotator true + } - false - }, - config = markdownAnnotatorConfig( - eolAsNewLine = true, - ), -) + false + }, + config = markdownAnnotatorConfig( + eolAsNewLine = true, + ), + ) +} @Composable private fun AnimeSummary( diff --git a/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt index 6fae10b5df..21574ff88b 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt @@ -17,11 +17,11 @@ import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.SwapCalls import androidx.compose.material.icons.outlined.TravelExplore import androidx.compose.material3.AlertDialog -import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SmallExtendedFloatingActionButton import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -115,7 +115,7 @@ fun SourcesScreen( floatingActionButton = { val buttonText = if (updateCount != 0) MR.strings.ext_update else MR.strings.ext_install val buttonIcon = if (updateCount != 0) Icons.Filled.Upload else Icons.Filled.Download - ExtendedFloatingActionButton( + SmallExtendedFloatingActionButton( text = { Text(text = stringResource(buttonText)) }, icon = { Icon(imageVector = buttonIcon, contentDescription = null) }, onClick = { toExtensionsScreen() }, diff --git a/app/src/main/java/eu/kanade/presentation/category/components/CategoryFloatingActionButton.kt b/app/src/main/java/eu/kanade/presentation/category/components/CategoryFloatingActionButton.kt index a151e9b2fb..bb246b066c 100644 --- a/app/src/main/java/eu/kanade/presentation/category/components/CategoryFloatingActionButton.kt +++ b/app/src/main/java/eu/kanade/presentation/category/components/CategoryFloatingActionButton.kt @@ -4,11 +4,11 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Add import androidx.compose.material3.Icon +import androidx.compose.material3.SmallExtendedFloatingActionButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import tachiyomi.i18n.MR -import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.util.shouldExpandFAB @@ -18,7 +18,7 @@ fun CategoryFloatingActionButton( onCreate: () -> Unit, modifier: Modifier = Modifier, ) { - ExtendedFloatingActionButton( + SmallExtendedFloatingActionButton( text = { Text(text = stringResource(MR.strings.action_add)) }, icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = null) }, onClick = onCreate, diff --git a/app/src/main/java/eu/kanade/presentation/components/AppBar.kt b/app/src/main/java/eu/kanade/presentation/components/AppBar.kt index b400917c99..1efeb34e30 100644 --- a/app/src/main/java/eu/kanade/presentation/components/AppBar.kt +++ b/app/src/main/java/eu/kanade/presentation/components/AppBar.kt @@ -21,8 +21,9 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.PlainTooltip import androidx.compose.material3.Text import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.TooltipAnchorPosition import androidx.compose.material3.TooltipBox -import androidx.compose.material3.TooltipDefaults +import androidx.compose.material3.TooltipDefaults.rememberTooltipPositionProvider import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior @@ -195,7 +196,7 @@ fun AppBarActions( actions.filterIsInstance().map { TooltipBox( - positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), + positionProvider = rememberTooltipPositionProvider(TooltipAnchorPosition.Above), tooltip = { PlainTooltip { Text(it.title) @@ -220,7 +221,7 @@ fun AppBarActions( val overflowActions = actions.filterIsInstance() if (overflowActions.isNotEmpty()) { TooltipBox( - positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), + positionProvider = rememberTooltipPositionProvider(TooltipAnchorPosition.Above), tooltip = { PlainTooltip { Text(stringResource(MR.strings.action_menu_overflow_description)) @@ -349,7 +350,7 @@ fun SearchToolbar( // Don't show search action } else if (searchQuery == null) { TooltipBox( - positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), + positionProvider = rememberTooltipPositionProvider(TooltipAnchorPosition.Above), tooltip = { PlainTooltip { Text(stringResource(MR.strings.action_search)) @@ -369,7 +370,7 @@ fun SearchToolbar( } } else if (searchQuery.isNotEmpty()) { TooltipBox( - positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), + positionProvider = rememberTooltipPositionProvider(TooltipAnchorPosition.Above), tooltip = { PlainTooltip { Text(stringResource(MR.strings.action_reset)) diff --git a/app/src/main/java/eu/kanade/presentation/components/FloatingActionAddButton.kt b/app/src/main/java/eu/kanade/presentation/components/FloatingActionAddButton.kt index 0bf59fbe18..3777fc7649 100644 --- a/app/src/main/java/eu/kanade/presentation/components/FloatingActionAddButton.kt +++ b/app/src/main/java/eu/kanade/presentation/components/FloatingActionAddButton.kt @@ -5,11 +5,11 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Add import androidx.compose.material3.Icon +import androidx.compose.material3.SmallExtendedFloatingActionButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import tachiyomi.i18n.MR -import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.util.shouldExpandFAB @@ -19,7 +19,7 @@ fun FloatingActionAddButton( onClick: () -> Unit, modifier: Modifier = Modifier, ) { - ExtendedFloatingActionButton( + SmallExtendedFloatingActionButton( text = { Text(text = stringResource(MR.strings.action_add)) }, icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = null) }, onClick = onClick, diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/AboutScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/AboutScreen.kt index ff5c69b1fa..1c50fc8318 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/AboutScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/AboutScreen.kt @@ -220,7 +220,6 @@ object AboutScreen : Screen() { is GetApplicationRelease.Result.OsTooOld -> { context.toast(MR.strings.update_check_eol) } - else -> {} } } catch (e: Exception) { context.toast(e.message) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/OpenSourceLicensesScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/OpenSourceLicensesScreen.kt index b879f0d2d0..3385f7430b 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/OpenSourceLicensesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/OpenSourceLicensesScreen.kt @@ -2,13 +2,16 @@ package eu.kanade.presentation.more.settings.screen.about import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow +import com.mikepenz.aboutlibraries.ui.compose.android.produceLibraries import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer import com.mikepenz.aboutlibraries.ui.compose.util.htmlReadyLicenseContent import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.util.Screen +import eu.kanade.tachiyomi.R import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.i18n.stringResource @@ -27,7 +30,9 @@ class OpenSourceLicensesScreen : Screen() { ) }, ) { contentPadding -> + val libraries by produceLibraries(R.raw.aboutlibraries) LibrariesContainer( + libraries = libraries, modifier = Modifier .fillMaxSize(), contentPadding = contentPadding, diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposDialogs.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposDialogs.kt index 39a57d4f84..bd7c1ceddd 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposDialogs.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposDialogs.kt @@ -1,6 +1,7 @@ package eu.kanade.presentation.more.settings.screen.browse.components import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.AlertDialog import androidx.compose.material3.OutlinedTextField @@ -61,6 +62,7 @@ fun ExtensionRepoCreateDialog( OutlinedTextField( modifier = Modifier + .fillMaxWidth() .focusRequester(focusRequester), value = name, onValueChange = { name = it }, diff --git a/app/src/main/java/eu/kanade/presentation/theme/TachiyomiTheme.kt b/app/src/main/java/eu/kanade/presentation/theme/TachiyomiTheme.kt index b84f9b1a03..5f1eb6b2dd 100644 --- a/app/src/main/java/eu/kanade/presentation/theme/TachiyomiTheme.kt +++ b/app/src/main/java/eu/kanade/presentation/theme/TachiyomiTheme.kt @@ -1,12 +1,14 @@ package eu.kanade.presentation.theme +import android.content.Context import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material.ripple.RippleAlpha import androidx.compose.material3.ColorScheme -import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MaterialExpressiveTheme import androidx.compose.material3.RippleConfiguration import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import eu.kanade.domain.ui.UiPreferences @@ -62,26 +64,36 @@ private fun BaseTachiyomiTheme( isAmoled: Boolean, content: @Composable () -> Unit, ) { - MaterialTheme( - colorScheme = getThemeColorScheme(appTheme, isAmoled), + val context = LocalContext.current + val isDark = isSystemInDarkTheme() + MaterialExpressiveTheme( + colorScheme = remember(appTheme, isDark, isAmoled) { + getThemeColorScheme( + context = context, + appTheme = appTheme, + isDark = isDark, + isAmoled = isAmoled, + ) + }, content = content, ) } -@Composable -@ReadOnlyComposable private fun getThemeColorScheme( + context: Context, appTheme: AppTheme, + isDark: Boolean, isAmoled: Boolean, ): ColorScheme { val colorScheme = if (appTheme == AppTheme.MONET) { - MonetColorScheme(LocalContext.current) + MonetColorScheme(context) } else { colorSchemes.getOrDefault(appTheme, TachiyomiColorScheme) } return colorScheme.getColorScheme( - isSystemInDarkTheme(), - isAmoled, + isDark = isDark, + isAmoled = isAmoled, + overrideDarkSurfaceContainers = appTheme != AppTheme.MONET, ) } diff --git a/app/src/main/java/eu/kanade/presentation/theme/colorscheme/BaseColorScheme.kt b/app/src/main/java/eu/kanade/presentation/theme/colorscheme/BaseColorScheme.kt index 22dd9a0a79..4ad2bfb807 100644 --- a/app/src/main/java/eu/kanade/presentation/theme/colorscheme/BaseColorScheme.kt +++ b/app/src/main/java/eu/kanade/presentation/theme/colorscheme/BaseColorScheme.kt @@ -14,16 +14,25 @@ internal abstract class BaseColorScheme { private val surfaceContainerHigh = Color(0xFF131313) private val surfaceContainerHighest = Color(0xFF1B1B1B) - fun getColorScheme(isDark: Boolean, isAmoled: Boolean): ColorScheme { + fun getColorScheme( + isDark: Boolean, + isAmoled: Boolean, + overrideDarkSurfaceContainers: Boolean, + ): ColorScheme { if (!isDark) return lightScheme if (!isAmoled) return darkScheme - return darkScheme.copy( + val amoledScheme = darkScheme.copy( background = Color.Black, onBackground = Color.White, surface = Color.Black, onSurface = Color.White, + ) + + if (!overrideDarkSurfaceContainers) return amoledScheme + + return amoledScheme.copy( surfaceVariant = surfaceContainer, // Navigation bar background (ThemePrefWidget) surfaceContainerLowest = surfaceContainer, surfaceContainerLow = surfaceContainer, diff --git a/app/src/main/java/eu/kanade/presentation/theme/colorscheme/MonetColorScheme.kt b/app/src/main/java/eu/kanade/presentation/theme/colorscheme/MonetColorScheme.kt index adcbaf62fa..039d4bd169 100644 --- a/app/src/main/java/eu/kanade/presentation/theme/colorscheme/MonetColorScheme.kt +++ b/app/src/main/java/eu/kanade/presentation/theme/colorscheme/MonetColorScheme.kt @@ -1,22 +1,17 @@ package eu.kanade.presentation.theme.colorscheme -import android.annotation.SuppressLint -import android.app.UiModeManager import android.app.WallpaperManager import android.content.Context -import android.graphics.Bitmap import android.os.Build import androidx.annotation.RequiresApi import androidx.compose.material3.ColorScheme import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.ui.graphics.Color -import androidx.core.content.getSystemService -import com.google.android.material.color.utilities.Hct -import com.google.android.material.color.utilities.MaterialDynamicColors -import com.google.android.material.color.utilities.QuantizerCelebi -import com.google.android.material.color.utilities.SchemeContent -import com.google.android.material.color.utilities.Score +import com.materialkolor.PaletteStyle +import com.materialkolor.dynamiccolor.ColorSpec +import com.materialkolor.ktx.DynamicScheme +import com.materialkolor.toColorScheme internal class MonetColorScheme(context: Context) : BaseColorScheme() { @@ -28,7 +23,7 @@ internal class MonetColorScheme(context: Context) : BaseColorScheme() { ?.primaryColor ?.toArgb() if (seed != null) { - MonetCompatColorScheme(context, seed) + MonetCompatColorScheme(Color(seed)) } else { TachiyomiColorScheme } @@ -41,19 +36,6 @@ internal class MonetColorScheme(context: Context) : BaseColorScheme() { override val lightScheme get() = monet.lightScheme - - companion object { - @Suppress("Unused") - @SuppressLint("RestrictedApi") - fun extractSeedColorFromImage(bitmap: Bitmap): Int? { - val width = bitmap.width - val height = bitmap.height - val bitmapPixels = IntArray(width * height) - bitmap.getPixels(bitmapPixels, 0, width, 0, 0, width, height) - return Score.score(QuantizerCelebi.quantize(bitmapPixels, 128), 1, 0)[0] - .takeIf { it != 0 } // Don't take fallback color - } - } } @RequiresApi(Build.VERSION_CODES.S) @@ -62,64 +44,19 @@ private class MonetSystemColorScheme(context: Context) : BaseColorScheme() { override val darkScheme = dynamicDarkColorScheme(context) } -private class MonetCompatColorScheme(context: Context, seed: Int) : BaseColorScheme() { - - override val lightScheme = generateColorSchemeFromSeed(context = context, seed = seed, dark = false) - override val darkScheme = generateColorSchemeFromSeed(context = context, seed = seed, dark = true) +internal class MonetCompatColorScheme(seed: Color) : BaseColorScheme() { + override val lightScheme = generateColorSchemeFromSeed(seed = seed, dark = false) + override val darkScheme = generateColorSchemeFromSeed(seed = seed, dark = true) companion object { - private fun Int.toComposeColor(): Color = Color(this) - - @SuppressLint("PrivateResource", "RestrictedApi") - private fun generateColorSchemeFromSeed(context: Context, seed: Int, dark: Boolean): ColorScheme { - val scheme = SchemeContent( - Hct.fromInt(seed), - dark, - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - context.getSystemService()?.contrast?.toDouble() ?: 0.0 - } else { - 0.0 - }, - ) - val dynamicColors = MaterialDynamicColors() - return ColorScheme( - primary = dynamicColors.primary().getArgb(scheme).toComposeColor(), - onPrimary = dynamicColors.onPrimary().getArgb(scheme).toComposeColor(), - primaryContainer = dynamicColors.primaryContainer().getArgb(scheme).toComposeColor(), - onPrimaryContainer = dynamicColors.onPrimaryContainer().getArgb(scheme).toComposeColor(), - inversePrimary = dynamicColors.inversePrimary().getArgb(scheme).toComposeColor(), - secondary = dynamicColors.secondary().getArgb(scheme).toComposeColor(), - onSecondary = dynamicColors.onSecondary().getArgb(scheme).toComposeColor(), - secondaryContainer = dynamicColors.secondaryContainer().getArgb(scheme).toComposeColor(), - onSecondaryContainer = dynamicColors.onSecondaryContainer().getArgb(scheme).toComposeColor(), - tertiary = dynamicColors.tertiary().getArgb(scheme).toComposeColor(), - onTertiary = dynamicColors.onTertiary().getArgb(scheme).toComposeColor(), - tertiaryContainer = dynamicColors.tertiary().getArgb(scheme).toComposeColor(), - onTertiaryContainer = dynamicColors.onTertiaryContainer().getArgb(scheme).toComposeColor(), - background = dynamicColors.background().getArgb(scheme).toComposeColor(), - onBackground = dynamicColors.onBackground().getArgb(scheme).toComposeColor(), - surface = dynamicColors.surface().getArgb(scheme).toComposeColor(), - onSurface = dynamicColors.onSurface().getArgb(scheme).toComposeColor(), - surfaceVariant = dynamicColors.surfaceVariant().getArgb(scheme).toComposeColor(), - onSurfaceVariant = dynamicColors.onSurfaceVariant().getArgb(scheme).toComposeColor(), - surfaceTint = dynamicColors.surfaceTint().getArgb(scheme).toComposeColor(), - inverseSurface = dynamicColors.inverseSurface().getArgb(scheme).toComposeColor(), - inverseOnSurface = dynamicColors.inverseOnSurface().getArgb(scheme).toComposeColor(), - error = dynamicColors.error().getArgb(scheme).toComposeColor(), - onError = dynamicColors.onError().getArgb(scheme).toComposeColor(), - errorContainer = dynamicColors.errorContainer().getArgb(scheme).toComposeColor(), - onErrorContainer = dynamicColors.onErrorContainer().getArgb(scheme).toComposeColor(), - outline = dynamicColors.outline().getArgb(scheme).toComposeColor(), - outlineVariant = dynamicColors.outlineVariant().getArgb(scheme).toComposeColor(), - scrim = Color.Black, - surfaceBright = dynamicColors.surfaceBright().getArgb(scheme).toComposeColor(), - surfaceDim = dynamicColors.surfaceDim().getArgb(scheme).toComposeColor(), - surfaceContainer = dynamicColors.surfaceContainer().getArgb(scheme).toComposeColor(), - surfaceContainerHigh = dynamicColors.surfaceContainerHigh().getArgb(scheme).toComposeColor(), - surfaceContainerHighest = dynamicColors.surfaceContainerHighest().getArgb(scheme).toComposeColor(), - surfaceContainerLow = dynamicColors.surfaceContainerLow().getArgb(scheme).toComposeColor(), - surfaceContainerLowest = dynamicColors.surfaceContainerLowest().getArgb(scheme).toComposeColor(), + fun generateColorSchemeFromSeed(seed: Color, dark: Boolean): ColorScheme { + return DynamicScheme( + seedColor = seed, + isDark = dark, + specVersion = ColorSpec.SpecVersion.SPEC_2025, + style = PaletteStyle.Expressive, ) + .toColorScheme(isAmoled = false) } } } diff --git a/app/src/main/java/eu/kanade/presentation/track/TrackerSearch.kt b/app/src/main/java/eu/kanade/presentation/track/TrackerSearch.kt index 02b14a7b4d..a291594d43 100644 --- a/app/src/main/java/eu/kanade/presentation/track/TrackerSearch.kt +++ b/app/src/main/java/eu/kanade/presentation/track/TrackerSearch.kt @@ -1,5 +1,6 @@ package eu.kanade.presentation.track +import android.content.ClipData import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -47,6 +48,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -55,10 +57,13 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.Clipboard import androidx.compose.ui.platform.ClipboardManager +import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.toClipEntry import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.capitalize import androidx.compose.ui.text.input.ImeAction @@ -73,6 +78,7 @@ import eu.kanade.presentation.components.DropdownMenu import eu.kanade.presentation.theme.TachiyomiPreviewTheme import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.util.system.openInBrowser +import kotlinx.coroutines.launch import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.ScrollbarLazyColumn import tachiyomi.presentation.core.components.material.Scaffold @@ -240,7 +246,7 @@ private fun SearchResultItem( onClick: () -> Unit, ) { val context = LocalContext.current - val clipboardManager: ClipboardManager = LocalClipboardManager.current + val clipboard: Clipboard = LocalClipboard.current val focusManager = LocalFocusManager.current val type = trackSearch.publishing_type.toLowerCase(Locale.current).capitalize(Locale.current) val status = trackSearch.publishing_status.toLowerCase(Locale.current).capitalize(Locale.current) @@ -248,6 +254,7 @@ private fun SearchResultItem( val shape = RoundedCornerShape(16.dp) val borderColor = if (selected) MaterialTheme.colorScheme.outline else Color.Transparent var dropDownMenuExpanded by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() Box( modifier = Modifier .fillMaxWidth() @@ -295,7 +302,13 @@ private fun SearchResultItem( expanded = dropDownMenuExpanded, onCollapseMenu = { dropDownMenuExpanded = false }, onCopyName = { - clipboardManager.setText(AnnotatedString(trackSearch.title)) + scope.launch { + val clipEntry = ClipData.newPlainText( + trackSearch.title, + trackSearch.title, + ).toClipEntry() + clipboard.setClipEntry(clipEntry) + } }, onOpenInBrowser = { val url = trackSearch.tracking_url diff --git a/app/src/main/java/eu/kanade/presentation/updates/UpdatesDialog.kt b/app/src/main/java/eu/kanade/presentation/updates/UpdatesDeleteConfirmationDialog.kt similarity index 100% rename from app/src/main/java/eu/kanade/presentation/updates/UpdatesDialog.kt rename to app/src/main/java/eu/kanade/presentation/updates/UpdatesDeleteConfirmationDialog.kt diff --git a/app/src/main/java/eu/kanade/presentation/updates/UpdatesFilterDialog.kt b/app/src/main/java/eu/kanade/presentation/updates/UpdatesFilterDialog.kt new file mode 100644 index 0000000000..f4644bf4bb --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/updates/UpdatesFilterDialog.kt @@ -0,0 +1,121 @@ +package eu.kanade.presentation.updates + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import eu.kanade.presentation.components.TabbedDialog +import eu.kanade.presentation.components.TabbedDialogPaddings +import eu.kanade.tachiyomi.ui.updates.UpdatesSettingsScreenModel +import kotlinx.collections.immutable.persistentListOf +import tachiyomi.core.common.preference.getAndSet +import tachiyomi.domain.updates.service.UpdatesPreferences +import tachiyomi.i18n.MR +import tachiyomi.i18n.animiru.AMMR +import tachiyomi.presentation.core.components.SettingsItemsPaddings +import tachiyomi.presentation.core.components.TriStateItem +import tachiyomi.presentation.core.components.material.padding +import tachiyomi.presentation.core.i18n.stringResource +import tachiyomi.presentation.core.util.collectAsState + +@Composable +fun UpdatesFilterDialog( + onDismissRequest: () -> Unit, + screenModel: UpdatesSettingsScreenModel, +) { + TabbedDialog( + onDismissRequest = onDismissRequest, + tabTitles = persistentListOf( + stringResource(MR.strings.action_filter), + ), + ) { + Column( + modifier = Modifier + .padding(vertical = TabbedDialogPaddings.Vertical) + .verticalScroll(rememberScrollState()), + ) { + FilterSheet(screenModel = screenModel) + } + } +} + +@Composable +private fun ColumnScope.FilterSheet( + screenModel: UpdatesSettingsScreenModel, +) { + val filterDownloaded by screenModel.updatesPreferences.filterDownloaded().collectAsState() + TriStateItem( + label = stringResource(MR.strings.label_downloaded), + state = filterDownloaded, + onClick = { screenModel.toggleFilter(UpdatesPreferences::filterDownloaded) }, + ) + + val filterUnseen by screenModel.updatesPreferences.filterUnseen().collectAsState() + TriStateItem( + label = stringResource(AMMR.strings.am_action_filter_unseen), + state = filterUnseen, + onClick = { screenModel.toggleFilter(UpdatesPreferences::filterUnseen) }, + ) + + val filterStarted by screenModel.updatesPreferences.filterStarted().collectAsState() + TriStateItem( + label = stringResource(MR.strings.label_started), + state = filterStarted, + onClick = { screenModel.toggleFilter(UpdatesPreferences::filterStarted) }, + ) + + val filterBookmarked by screenModel.updatesPreferences.filterBookmarked().collectAsState() + TriStateItem( + label = stringResource(MR.strings.action_filter_bookmarked), + state = filterBookmarked, + onClick = { screenModel.toggleFilter(UpdatesPreferences::filterBookmarked) }, + ) + + // AM --> + val filterFillermarked by screenModel.updatesPreferences.filterFillermarked().collectAsState() + TriStateItem( + label = stringResource(AMMR.strings.action_filter_fillermarked), + state = filterFillermarked, + onClick = { screenModel.toggleFilter(UpdatesPreferences::filterFillermarked) }, + ) + // <-- AM + + HorizontalDivider(modifier = Modifier.padding(MaterialTheme.padding.small)) + + val filterExcludedScanlators by screenModel.updatesPreferences.filterExcludedScanlators().collectAsState() + + fun toggleScanlatorFilter() = screenModel.updatesPreferences.filterExcludedScanlators().getAndSet { !it } + + Row( + modifier = Modifier + .clickable { toggleScanlatorFilter() } + .fillMaxWidth() + .padding(horizontal = SettingsItemsPaddings.Horizontal), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = stringResource(MR.strings.action_filter_excluded_scanlators), + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodyMedium, + ) + + Switch( + checked = filterExcludedScanlators, + onCheckedChange = { toggleScanlatorFilter() }, + ) + } +} diff --git a/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt b/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt index 5bb0a46d4a..07fb30910d 100644 --- a/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt @@ -5,9 +5,12 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.CalendarMonth +import androidx.compose.material.icons.outlined.FilterList import androidx.compose.material.icons.outlined.FlipToBack import androidx.compose.material.icons.outlined.Refresh import androidx.compose.material.icons.outlined.SelectAll +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -35,6 +38,7 @@ import tachiyomi.presentation.core.components.material.PullRefresh import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.LoadingScreen +import tachiyomi.presentation.core.theme.active import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.time.LocalDate @@ -100,6 +104,8 @@ fun UpdateScreen( fun UpdatesTopBar( onCalendarClicked: () -> Unit, onUpdateLibrary: () -> Unit, + onFilterClicked: () -> Unit, + hasFilters: Boolean, // For action mode actionModeCounter: Int, onSelectAll: () -> Unit, @@ -114,6 +120,12 @@ fun UpdatesTopBar( actions = { AppBarActions( persistentListOf( + AppBar.Action( + title = stringResource(MR.strings.action_filter), + icon = Icons.Outlined.FilterList, + iconTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current, + onClick = onFilterClicked, + ), AppBar.Action( title = stringResource(MR.strings.action_view_upcoming), icon = Icons.Outlined.CalendarMonth, diff --git a/app/src/main/java/eu/kanade/presentation/util/ExceptionFormatter.kt b/app/src/main/java/eu/kanade/presentation/util/ExceptionFormatter.kt index e4e4eed039..db470a0f4b 100644 --- a/app/src/main/java/eu/kanade/presentation/util/ExceptionFormatter.kt +++ b/app/src/main/java/eu/kanade/presentation/util/ExceptionFormatter.kt @@ -9,21 +9,21 @@ import tachiyomi.domain.source.model.SourceNotInstalledException import tachiyomi.i18n.MR import java.net.UnknownHostException -context(Context) +context(context: Context) val Throwable.formattedMessage: String get() { when (this) { - is HttpException -> return stringResource(MR.strings.exception_http, code) + is HttpException -> return context.stringResource(MR.strings.exception_http, code) is UnknownHostException -> { - return if (!isOnline()) { - stringResource(MR.strings.exception_offline) + return if (!context.isOnline()) { + context.stringResource(MR.strings.exception_offline) } else { - stringResource(MR.strings.exception_unknown_host, message ?: "") + context.stringResource(MR.strings.exception_unknown_host, message ?: "") } } - is NoResultsException -> return stringResource(MR.strings.no_results_found) - is SourceNotInstalledException -> return stringResource(MR.strings.loader_not_implemented_error) + is NoResultsException -> return context.stringResource(MR.strings.no_results_found) + is SourceNotInstalledException -> return context.stringResource(MR.strings.loader_not_implemented_error) } return when (val className = this::class.simpleName) { "Exception", "IOException" -> message ?: className diff --git a/app/src/main/java/eu/kanade/presentation/util/FastScrollAnimateItem.kt b/app/src/main/java/eu/kanade/presentation/util/FastScrollAnimateItem.kt index a6c9f70198..9d24b8520e 100644 --- a/app/src/main/java/eu/kanade/presentation/util/FastScrollAnimateItem.kt +++ b/app/src/main/java/eu/kanade/presentation/util/FastScrollAnimateItem.kt @@ -4,5 +4,7 @@ import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.ui.Modifier // https://issuetracker.google.com/352584409 -context(LazyItemScope) -fun Modifier.animateItemFastScroll() = this.animateItem(fadeInSpec = null, fadeOutSpec = null) +context(itemScope: LazyItemScope) +fun Modifier.animateItemFastScroll() = with(itemScope) { + this@animateItemFastScroll.animateItem(fadeInSpec = null, fadeOutSpec = null) +} diff --git a/app/src/main/java/eu/kanade/presentation/webview/WebViewScreenContent.kt b/app/src/main/java/eu/kanade/presentation/webview/WebViewScreenContent.kt index af0f238919..1dfab78af3 100644 --- a/app/src/main/java/eu/kanade/presentation/webview/WebViewScreenContent.kt +++ b/app/src/main/java/eu/kanade/presentation/webview/WebViewScreenContent.kt @@ -273,7 +273,7 @@ fun WebViewScreenContent( .align(Alignment.BottomCenter), ) is LoadingState.Loading -> LinearProgressIndicator( - progress = { (loadingState as? LoadingState.Loading)?.progress ?: 1f }, + progress = { loadingState.progress }, modifier = Modifier .fillMaxWidth() .align(Alignment.BottomCenter), diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt index f0c9564e99..066cda1936 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/App.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt @@ -144,8 +144,14 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor startSyncJob(syncPreferences.getSyncTriggerOptions().syncOnAppStart) // <-- AM (SYNC) - if (!LogcatLogger.isInstalled && networkPreferences.verboseLogging().get()) { - LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE)) + if (!LogcatLogger.isInstalled) { + val minLogPriority = when { + networkPreferences.verboseLogging().get() -> LogPriority.VERBOSE + BuildConfig.DEBUG -> LogPriority.DEBUG + else -> LogPriority.INFO + } + LogcatLogger.install() + LogcatLogger.loggers += AndroidLogcatLogger(minLogPriority) } initializeMigrator() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt index 055c882ab4..81e39db7e9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt @@ -41,9 +41,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { suspend fun addLibAnime(track: Track): Track { return withIOContext { - val query = """ - |mutation AddAnime(${'$'}animeId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}private: Boolean) { - |SaveMediaListEntry (mediaId: ${'$'}animeId, progress: ${'$'}progress, status: ${'$'}status, private: ${'$'}private) { + val query = $$""" + |mutation AddAnime($animeId: Int, $progress: Int, $status: MediaListStatus, $private: Boolean) { + |SaveMediaListEntry (mediaId: $animeId, progress: $progress, status: $status, private: $private) { | id | status |} @@ -78,14 +78,14 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { suspend fun updateLibAnime(track: Track): Track { return withIOContext { - val query = """ + val query = $$""" |mutation UpdateAnime( - |${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}private: Boolean, - |${'$'}score: Int, ${'$'}startedAt: FuzzyDateInput, ${'$'}completedAt: FuzzyDateInput + |$listId: Int, $progress: Int, $status: MediaListStatus, $private: Boolean, + |$score: Int, $startedAt: FuzzyDateInput, $completedAt: FuzzyDateInput |) { |SaveMediaListEntry( - |id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, private: ${'$'}private, - |scoreRaw: ${'$'}score, startedAt: ${'$'}startedAt, completedAt: ${'$'}completedAt + |id: $listId, progress: $progress, status: $status, private: $private, + |scoreRaw: $score, startedAt: $startedAt, completedAt: $completedAt |) { |id |status @@ -114,9 +114,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { suspend fun deleteLibAnime(track: DomainTrack) { withIOContext { - val query = """ - |mutation DeleteAnime(${'$'}listId: Int) { - |DeleteMediaListEntry(id: ${'$'}listId) { + val query = $$""" + |mutation DeleteAnime($listId: Int) { + |DeleteMediaListEntry(id: $listId) { |deleted |} |} @@ -135,10 +135,10 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { suspend fun search(search: String): List { return withIOContext { - val query = """ - |query Search(${'$'}query: String) { + val query = $$""" + |query Search($query: String) { |Page (perPage: 50) { - |media(search: ${'$'}query, type: ANIME) { + |media(search: $query, type: ANIME) { |id |studios { |edges { @@ -192,10 +192,10 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { suspend fun findLibAnime(track: Track, userid: Int): Track? { return withIOContext { - val query = """ - |query (${'$'}id: Int!, ${'$'}anime_id: Int!) { + val query = $$""" + |query ($id: Int!, $anime_id: Int!) { |Page { - |mediaList(userId: ${'$'}id, type: ANIME, mediaId: ${'$'}anime_id) { + |mediaList(userId: $id, type: ANIME, mediaId: $anime_id) { |id |status |scoreRaw: score(format: POINT_100) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt index e30972cecb..dcf766b914 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt @@ -109,7 +109,7 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") { // Users can set a 'username' (not nickname) once which effectively // replaces the stringified ID in certain queries. // If no username is set, the API returns the user ID as a strings - var username = api.getUsername() + val username = api.getUsername() saveCredentials(username, oauth.accessToken) } catch (_: Throwable) { logout() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/jellyfin/Jellyfin.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/jellyfin/Jellyfin.kt index 50fb293e28..d25efa7002 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/jellyfin/Jellyfin.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/jellyfin/Jellyfin.kt @@ -85,7 +85,7 @@ class Jellyfin(id: Long) : BaseTracker(id, "Jellyfin"), EnhancedTracker { override suspend fun match(anime: Anime): TrackSearch? = try { api.getTrackSearch(anime.url) - } catch (e: Exception) { + } catch (_: Exception) { null } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt index 0728b7a02d..0f1b19fe3f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt @@ -10,15 +10,12 @@ import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALListItemStatus import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALOAuth import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALSearchResult import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALUser -import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALUserSearchResult import eu.kanade.tachiyomi.network.DELETE import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.util.PkceUtil -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll import kotlinx.serialization.json.Json import okhttp3.FormBody import okhttp3.Headers @@ -78,14 +75,14 @@ class MyAnimeListApi( // MAL API throws a 400 when the query is over 64 characters... .appendQueryParameter("q", query.take(64)) .appendQueryParameter("nsfw", "true") + .appendQueryParameter("fields", SEARCH_FIELDS) .build() with(json) { authClient.newCall(GET(url.toString())) .awaitSuccess() .parseAs() .data - .map { async { getAnimeDetails(it.node.id) } } - .awaitAll() + .map { parseSearchItem(it.node) } } } } @@ -94,29 +91,13 @@ class MyAnimeListApi( return withIOContext { val url = "$BASE_API_URL/anime".toUri().buildUpon() .appendPath(id.toString()) - .appendQueryParameter( - "fields", - "id,title,synopsis,num_episodes,mean,main_picture,status,media_type,start_date", - ) + .appendQueryParameter("fields", SEARCH_FIELDS) .build() with(json) { authClient.newCall(GET(url.toString())) .awaitSuccess() .parseAs() - .let { - TrackSearch.create(trackId).apply { - remote_id = it.id - title = it.title - summary = it.synopsis - total_episodes = it.numEpisodes - score = it.mean - cover_url = it.covers?.large.orEmpty() - tracking_url = "https://myanimelist.net/anime/$remote_id" - publishing_status = it.status.replace("_", " ") - publishing_type = it.mediaType.replace("_", " ") - start_date = it.startDate ?: "" - } - } + .let { parseSearchItem(it) } } } } @@ -180,8 +161,7 @@ class MyAnimeListApi( val matches = myListSearchResult.data .filter { it.node.title.contains(query, ignoreCase = true) } - .map { async { getAnimeDetails(it.node.id) } } - .awaitAll() + .map { parseSearchItem(it.node) } // Check next page if there's more if (!myListSearchResult.paging.next.isNullOrBlank()) { @@ -192,10 +172,10 @@ class MyAnimeListApi( } } - private suspend fun getListPage(offset: Int): MALUserSearchResult { + private suspend fun getListPage(offset: Int): MALSearchResult { return withIOContext { val urlBuilder = "$BASE_API_URL/users/@me/animelist".toUri().buildUpon() - .appendQueryParameter("fields", "list_status{start_date,finish_date}") + .appendQueryParameter("fields", SEARCH_FIELDS) .appendQueryParameter("limit", LIST_PAGINATION_AMOUNT.toString()) if (offset > 0) { urlBuilder.appendQueryParameter("offset", offset.toString()) @@ -224,6 +204,22 @@ class MyAnimeListApi( } } + private fun parseSearchItem(searchItem: MALAnime): TrackSearch { + return TrackSearch.create(trackId).apply { + remote_id = searchItem.id + title = searchItem.title + summary = searchItem.synopsis + total_episodes = searchItem.numEpisodes + score = searchItem.mean + cover_url = searchItem.covers?.large.orEmpty() + tracking_url = "https://myanimelist.net/anime/$remote_id" + publishing_status = searchItem.status.replace("_", " ") + publishing_type = searchItem.mediaType.replace("_", " ") + start_date = searchItem.startDate ?: "" + authors = searchItem.studios.map { it.name } + } + } + private fun parseDate(isoDate: String): Long { return SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(isoDate)?.time ?: 0L } @@ -235,7 +231,7 @@ class MyAnimeListApi( return try { val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US) outputDf.format(epochTime) - } catch (e: Exception) { + } catch (_: Exception) { null } } @@ -246,6 +242,9 @@ class MyAnimeListApi( private const val BASE_OAUTH_URL = "https://myanimelist.net/v1/oauth2" private const val BASE_API_URL = "https://api.myanimelist.net/v2" + private const val SEARCH_FIELDS = + "id,title,synopsis,num_episodes,mean,main_picture,status,media_type,start_date,studios" + private const val LIST_PAGINATION_AMOUNT = 250 private var codeVerifier: String = "" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALAnime.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALAnime.kt index 022cb6c099..6ce987cfde 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALAnime.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALAnime.kt @@ -18,6 +18,12 @@ data class MALAnime( val mediaType: String, @SerialName("start_date") val startDate: String?, + val studios: List = emptyList(), +) + +@Serializable +data class MALStudioNode( + val name: String, ) @Serializable diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALSearch.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALSearch.kt index 51ef2a6a48..7ce9a867da 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALSearch.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALSearch.kt @@ -5,14 +5,15 @@ import kotlinx.serialization.Serializable @Serializable data class MALSearchResult( val data: List, + val paging: MALSearchPaging, ) @Serializable data class MALSearchResultNode( - val node: MALSearchResultItem, + val node: MALAnime, ) @Serializable -data class MALSearchResultItem( - val id: Int, +data class MALSearchPaging( + val next: String?, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALUserListSearch.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALUserListSearch.kt deleted file mode 100644 index fad099a24b..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALUserListSearch.kt +++ /dev/null @@ -1,25 +0,0 @@ -package eu.kanade.tachiyomi.data.track.myanimelist.dto - -import kotlinx.serialization.Serializable - -@Serializable -data class MALUserSearchResult( - val data: List, - val paging: MALUserSearchPaging, -) - -@Serializable -data class MALUserSearchItem( - val node: MALUserSearchItemNode, -) - -@Serializable -data class MALUserSearchPaging( - val next: String?, -) - -@Serializable -data class MALUserSearchItemNode( - val id: Int, - val title: String, -) diff --git a/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt b/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt index 8d1c847f80..8acb326277 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt @@ -23,6 +23,7 @@ import tachiyomi.domain.backup.service.BackupPreferences import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.storage.service.StoragePreferences +import tachiyomi.domain.updates.service.UpdatesPreferences import uy.kohesive.injekt.api.InjektModule import uy.kohesive.injekt.api.InjektRegistrar import uy.kohesive.injekt.api.addSingletonFactory @@ -49,6 +50,9 @@ class PreferenceModule(val app: Application) : InjektModule { addSingletonFactory { LibraryPreferences(get()) } + addSingletonFactory { + UpdatesPreferences(get()) + } // AY --> addSingletonFactory { PlayerPreferences(get()) diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/ShizukuInstaller.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/ShizukuInstaller.kt index 303a2dad88..834a40d6fa 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/ShizukuInstaller.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/ShizukuInstaller.kt @@ -109,9 +109,9 @@ class ShizukuInstaller(private val service: Service) : Installer(service) { override fun processEntry(entry: Entry) { super.processEntry(entry) try { - shellInterface?.install( - service.contentResolver.openAssetFileDescriptor(entry.uri, "r"), - ) + service.contentResolver.openAssetFileDescriptor(entry.uri, "r").use { + shellInterface?.install(it) + } } catch (e: Exception) { logcat(LogPriority.ERROR, e) { "Failed to install extension ${entry.downloadId} ${entry.uri}" } continueQueue(InstallStep.Error) @@ -124,7 +124,13 @@ class ShizukuInstaller(private val service: Service) : Installer(service) { override fun onDestroy() { Shizuku.removeBinderDeadListener(shizukuDeadListener) Shizuku.removeRequestPermissionResultListener(shizukuPermissionListener) - Shizuku.unbindUserService(shizukuArgs, connection, true) + if (Shizuku.pingBinder()) { + try { + Shizuku.unbindUserService(shizukuArgs, connection, true) + } catch (e: Exception) { + logcat(LogPriority.WARN, e) { "Failed to unbind shizuku service" } + } + } service.unregisterReceiver(receiver) logcat { "ShizukuInstaller destroy" } scope.cancel() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt index b259c8f327..b4c420ab7b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt @@ -54,80 +54,47 @@ class ExtensionsScreenModel( ExtensionUiModel.Item(it, map[it.pkgName] ?: InstallStep.Idle) } } - val queryFilter: (String) -> ((Extension) -> Boolean) = { query -> - filter@{ extension -> - if (query.isEmpty()) return@filter true - query.split(",").any { _input -> - val input = _input.trim() - if (input.isEmpty()) return@any false - when (extension) { - is Extension.Available -> { - extension.sources.any { - it.name.contains(input, ignoreCase = true) || - it.baseUrl.contains(input, ignoreCase = true) || - it.id == input.toLongOrNull() - } || - extension.name.contains(input, ignoreCase = true) - } - is Extension.Installed -> { - extension.sources.any { - it.name.contains(input, ignoreCase = true) || - it.id == input.toLongOrNull() || - if (it is AnimeHttpSource) { - it.baseUrl.contains(input, ignoreCase = true) - } else { - false - } - } || - extension.name.contains(input, ignoreCase = true) - } - is Extension.Untrusted -> extension.name.contains(input, ignoreCase = true) - } - } - } - } screenModelScope.launchIO { combine( - state.map { it.searchQuery }.distinctUntilChanged().debounce(SEARCH_DEBOUNCE_MILLIS), + state.map { it.searchQuery } + .distinctUntilChanged() + .debounce(SEARCH_DEBOUNCE_MILLIS) + .map { searchQueryPredicate(it ?: "") }, currentDownloads, getExtensions.subscribe(), - ) { query, downloads, (_updates, _, _available, _untrusted) -> - val searchQuery = query ?: "" - - val itemsGroups: ItemGroups = mutableMapOf() - - val updates = _updates.filter(queryFilter(searchQuery)).map(extensionMapper(downloads)) - if (updates.isNotEmpty()) { - itemsGroups[ExtensionUiModel.Header.Resource(MR.strings.ext_updates_pending)] = updates - } + ) { predicate, downloads, (_updates, _installed, _available, _untrusted) -> + buildMap { + val updates = _updates.filter(predicate).map(extensionMapper(downloads)) + if (updates.isNotEmpty()) { + put(ExtensionUiModel.Header.Resource(MR.strings.ext_updates_pending), updates) + } - val untrusted = _untrusted.filter(queryFilter(searchQuery)).map(extensionMapper(downloads)) - // AM (BROWSE) --> - if (untrusted.isNotEmpty()) { - itemsGroups[ExtensionUiModel.Header.Resource(MR.strings.ext_untrusted)] = untrusted - } - // <-- AM (BROWSE) - - val languagesWithExtensions = _available - .filter(queryFilter(searchQuery)) - .groupBy { it.lang } - .toSortedMap(LocaleHelper.comparator) - .map { (lang, exts) -> - ExtensionUiModel.Header.Text(LocaleHelper.getSourceDisplayName(lang, context)) to - exts.map(extensionMapper(downloads)) + val untrusted = _untrusted.filter(predicate).map(extensionMapper(downloads)) + // AM (BROWSE) --> + if (untrusted.isNotEmpty()) { + put(ExtensionUiModel.Header.Resource(MR.strings.ext_untrusted), untrusted) + } + // <-- AM (BROWSE) + + val languagesWithExtensions = _available + .filter(predicate) + .groupBy { it.lang } + .toSortedMap(LocaleHelper.comparator) + .map { (lang, exts) -> + ExtensionUiModel.Header.Text(LocaleHelper.getSourceDisplayName(lang, context)) to + exts.map(extensionMapper(downloads)) + } + if (languagesWithExtensions.isNotEmpty()) { + putAll(languagesWithExtensions) } - if (languagesWithExtensions.isNotEmpty()) { - itemsGroups.putAll(languagesWithExtensions) } - - itemsGroups } - .collectLatest { + .collectLatest { items -> mutableState.update { state -> state.copy( isLoading = false, - items = it, + items = items, ) } } @@ -144,6 +111,36 @@ class ExtensionsScreenModel( .launchIn(screenModelScope) } + fun searchQueryPredicate(query: String): (Extension) -> Boolean { + val subqueries = query.split(",") + .map { it.trim() } + .filterNot { it.isBlank() } + + if (subqueries.isEmpty()) return { true } + + return { extension -> + subqueries.any { subquery -> + if (extension.name.contains(subquery, ignoreCase = true)) return@any true + + when (extension) { + is Extension.Installed -> extension.sources.any { source -> + source.name.contains(subquery, ignoreCase = true) || + (source as? AnimeHttpSource)?.baseUrl?.contains(subquery, ignoreCase = true) == true || + source.id == subquery.toLongOrNull() + } + + is Extension.Available -> extension.sources.any { + it.name.contains(subquery, ignoreCase = true) || + it.baseUrl.contains(subquery, ignoreCase = true) || + it.id == subquery.toLongOrNull() + } + + else -> false + } + } + } + } + fun search(query: String?) { mutableState.update { it.copy(searchQuery = query) @@ -227,7 +224,7 @@ class ExtensionsScreenModel( } } -typealias ItemGroups = MutableMap> +typealias ItemGroups = Map> object ExtensionUiModel { sealed interface Header { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/anime/MigrateAnimeScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/anime/MigrateAnimeScreen.kt index b447c889a6..0e3e6b6279 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/anime/MigrateAnimeScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/anime/MigrateAnimeScreen.kt @@ -9,11 +9,14 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ArrowForward import androidx.compose.material3.Icon +import androidx.compose.material3.SmallExtendedFloatingActionButton import androidx.compose.material3.Text +import androidx.compose.material3.animateFloatingActionButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import cafe.adriel.voyager.core.model.rememberScreenModel @@ -29,7 +32,6 @@ import mihon.feature.migration.config.MigrationConfigScreen import tachiyomi.domain.anime.model.Anime import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.FastScrollLazyColumn -import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.screens.EmptyScreen @@ -75,20 +77,22 @@ data class MigrateAnimeScreen( ) }, floatingActionButton = { - if (state.selectionMode) { - ExtendedFloatingActionButton( - text = { Text(text = stringResource(MR.strings.migrationConfigScreen_continueButtonText)) }, - icon = { - Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null) - }, - onClick = { - val selection = state.selection - screenModel.clearSelection() - navigator.push(MigrationConfigScreen(selection)) - }, - expanded = lazyListState.shouldExpandFAB(), - ) - } + SmallExtendedFloatingActionButton( + text = { Text(text = stringResource(MR.strings.migrationConfigScreen_continueButtonText)) }, + icon = { + Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null) + }, + onClick = { + val selection = state.selection + screenModel.clearSelection() + navigator.push(MigrationConfigScreen(selection)) + }, + expanded = lazyListState.shouldExpandFAB(), + modifier = Modifier.animateFloatingActionButton( + visible = state.selectionMode, + alignment = Alignment.BottomEnd, + ), + ) }, ) { contentPadding -> if (state.isEmpty) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSourceSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSourceSearchScreen.kt index 8cc5d959b8..f32ab31ace 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSourceSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSourceSearchScreen.kt @@ -1,17 +1,20 @@ package eu.kanade.tachiyomi.ui.browse.migration.search -import androidx.compose.animation.AnimatedVisibility import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.FilterList import androidx.compose.material3.Icon +import androidx.compose.material3.SmallExtendedFloatingActionButton import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text +import androidx.compose.material3.animateFloatingActionButton import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalUriHandler import cafe.adriel.voyager.core.model.rememberScreenModel @@ -35,7 +38,6 @@ import mihon.presentation.core.util.collectAsLazyPagingItems import tachiyomi.core.common.Constants import tachiyomi.domain.anime.model.Anime import tachiyomi.i18n.MR -import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.screens.LoadingScreen @@ -74,13 +76,15 @@ data class MigrateSourceSearchScreen( ) }, floatingActionButton = { - AnimatedVisibility(visible = state.filters.isNotEmpty()) { - ExtendedFloatingActionButton( - text = { Text(text = stringResource(MR.strings.action_filter)) }, - icon = { Icon(Icons.Outlined.FilterList, contentDescription = null) }, - onClick = screenModel::openFilterSheet, - ) - } + SmallExtendedFloatingActionButton( + text = { Text(text = stringResource(MR.strings.action_filter)) }, + icon = { Icon(Icons.Outlined.FilterList, contentDescription = null) }, + onClick = screenModel::openFilterSheet, + modifier = Modifier.animateFloatingActionButton( + visible = state.filters.isNotEmpty(), + alignment = Alignment.BottomEnd, + ), + ) }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, ) { paddingValues -> diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueScreen.kt index ce075b79d5..2db02cf39d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueScreen.kt @@ -16,6 +16,7 @@ import androidx.compose.material.icons.outlined.Pause import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SmallExtendedFloatingActionButton import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState @@ -57,7 +58,6 @@ import tachiyomi.core.common.util.lang.launchUI import tachiyomi.i18n.MR import tachiyomi.i18n.aniyomi.AYMR import tachiyomi.presentation.core.components.Pill -import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.screens.EmptyScreen @@ -208,7 +208,7 @@ object DownloadQueueScreen : Screen() { exit = fadeOut(), ) { val isRunning by screenModel.isDownloaderRunning.collectAsState() - ExtendedFloatingActionButton( + SmallExtendedFloatingActionButton( text = { // AY --> val id = if (isRunning) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt index c3559d28c6..58ccaae44f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt @@ -3,9 +3,12 @@ package eu.kanade.tachiyomi.ui.library import eu.kanade.tachiyomi.source.getNameForAnimeInfo import tachiyomi.domain.library.model.LibraryAnime import tachiyomi.domain.source.service.SourceManager +import tachiyomi.source.local.LocalSource import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +private const val LOCAL_SOURCE_ID_ALIAS = "local" + data class LibraryItem( val libraryAnime: LibraryAnime, val downloadCount: Long = -1, @@ -23,9 +26,17 @@ data class LibraryItem( * @return true if the anime matches the query, false otherwise. */ fun matches(constraint: String): Boolean { - val sourceName by lazy { sourceManager.getOrStub(libraryAnime.anime.source).getNameForAnimeInfo() } + val source = sourceManager.getOrStub(libraryAnime.anime.source) + val sourceName by lazy { source.getNameForAnimeInfo() } if (constraint.startsWith("id:", true)) { return id == constraint.substringAfter("id:").toLongOrNull() + } else if (constraint.startsWith("src:", true)) { + val querySource = constraint.substringAfter("src:") + return if (querySource.equals(LOCAL_SOURCE_ID_ALIAS, ignoreCase = true)) { + source.id == LocalSource.ID + } else { + source.id == querySource.toLongOrNull() + } } return libraryAnime.anime.title.contains(constraint, true) || (libraryAnime.anime.author?.contains(constraint, true) ?: false) || diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsTab.kt index 15f8277e9e..4984db7acb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsTab.kt @@ -54,6 +54,7 @@ import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences import eu.kanade.tachiyomi.ui.updates.AnimeUpdatesHalfTab import eu.kanade.tachiyomi.ui.updates.UpdatesScreenModel +import eu.kanade.tachiyomi.ui.updates.UpdatesSettingsScreenModel import eu.kanade.tachiyomi.ui.updates.openEpisode import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel @@ -106,6 +107,7 @@ data object RecentsTab : Tab { val historyScreenModel = rememberScreenModel { HistoryScreenModel() } // AM (RECENTS_FILTER_CHIP) --> val updatesScreenModel = rememberScreenModel { UpdatesScreenModel() } + val updatesSettingsScreenModel = rememberScreenModel { UpdatesSettingsScreenModel() } // AM (TAB_HOLD) --> val snackbarHostState = SnackbarHostState() // <-- AM (TAB_HOLD) @@ -120,7 +122,7 @@ data object RecentsTab : Tab { ) { contentPadding -> Crossfade(targetState = showHistoryScreen, label = "recents_crossfade") { showHistory -> if (!showHistory) { - AnimeUpdatesHalfTab(updatesScreenModel, contentPadding) + AnimeUpdatesHalfTab(updatesScreenModel, updatesSettingsScreenModel, contentPadding) } else { HistoryHalfTab(historyScreenModel, snackbarHostState, contentPadding) } @@ -182,6 +184,8 @@ fun RecentsScaffold( if (!showHistoryScreen) { UpdatesTopBar( onCalendarClicked = { navigator.push(UpcomingScreen()) }, + onFilterClicked = updatesScreenModel::showFilterDialog, + hasFilters = updatesState.hasActiveFilters, onUpdateLibrary = updatesScreenModel::updateLibrary, actionModeCounter = updatesState.selected.size, onSelectAll = { updatesScreenModel.toggleAllSelection(true) }, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt index e1558ac0bb..3d40e19861 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt @@ -4,6 +4,7 @@ import android.app.Application import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Immutable import androidx.compose.runtime.getValue +import androidx.compose.ui.util.fastFilter import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.screenModelScope import eu.kanade.core.preference.asState @@ -27,15 +28,21 @@ import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import logcat.LogPriority +import tachiyomi.core.common.preference.TriState import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchNonCancellable import tachiyomi.core.common.util.system.logcat import tachiyomi.domain.anime.interactor.GetAnime +import tachiyomi.domain.anime.model.applyFilter import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.episode.interactor.GetEpisode import tachiyomi.domain.episode.interactor.UpdateEpisode @@ -44,6 +51,7 @@ import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.updates.interactor.GetUpdates import tachiyomi.domain.updates.model.UpdatesWithRelations +import tachiyomi.domain.updates.service.UpdatesPreferences import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.time.ZonedDateTime @@ -58,6 +66,7 @@ class UpdatesScreenModel( private val getAnime: GetAnime = Injekt.get(), private val getEpisode: GetEpisode = Injekt.get(), private val libraryPreferences: LibraryPreferences = Injekt.get(), + private val updatesPreferences: UpdatesPreferences = Injekt.get(), val snackbarHostState: SnackbarHostState = SnackbarHostState(), // AY --> downloadPreferences: DownloadPreferences = Injekt.get(), @@ -83,19 +92,36 @@ class UpdatesScreenModel( val limit = ZonedDateTime.now().minusMonths(3).toInstant() combine( - getUpdates.subscribe(limit).distinctUntilChanged(), + // needed for SQL filters (unread, started, bookmarked, etc) + getUpdatesItemPreferenceFlow() + .distinctUntilChanged() + .flatMapLatest { + getUpdates.subscribe( + limit, + unseen = it.filterUnseen.toBooleanOrNull(), + started = it.filterStarted.toBooleanOrNull(), + bookmarked = it.filterBookmarked.toBooleanOrNull(), + fillermarked = it.filterFillermarked.toBooleanOrNull(), + hideExcludedScanlators = it.filterExcludedScanlators, + ).distinctUntilChanged() + }, downloadCache.changes, downloadManager.queueState, - ) { updates, _, _ -> updates } - .catch { - logcat(LogPriority.ERROR, it) - _events.send(Event.InternalError) - } - .collectLatest { updates -> + // needed for Kotlin filters (downloaded) + getUpdatesItemPreferenceFlow().distinctUntilChanged { old, new -> + old.filterDownloaded == new.filterDownloaded + }, + ) { updates, _, _, itemPreferences -> + updates + .toUpdateItems() + .applyFilters(itemPreferences) + .toPersistentList() + } + .collectLatest { updateItems -> mutableState.update { it.copy( isLoading = false, - items = updates.toUpdateItems(), + items = updateItems, ) } } @@ -106,9 +132,44 @@ class UpdatesScreenModel( .catch { logcat(LogPriority.ERROR, it) } .collect(this@UpdatesScreenModel::updateDownloadState) } + + getUpdatesItemPreferenceFlow() + .map { prefs -> + listOf( + prefs.filterUnseen, + prefs.filterDownloaded, + prefs.filterStarted, + prefs.filterBookmarked, + prefs.filterFillermarked, + ) + .any { it != TriState.DISABLED } + } + .distinctUntilChanged() + .onEach { + mutableState.update { state -> + state.copy(hasActiveFilters = it) + } + } + .launchIn(screenModelScope) } - private fun List.toUpdateItems(): PersistentList { + private fun List.applyFilters( + preferences: ItemPreferences, + ): List { + val filterDownloaded = preferences.filterDownloaded + + val filterFnDownloaded: (UpdatesItem) -> Boolean = { + applyFilter(filterDownloaded) { + it.downloadStateProvider() == Download.State.DOWNLOADED + } + } + + return fastFilter { + filterFnDownloaded(it) + } + } + + private fun List.toUpdateItems(): List { return this .map { update -> val activeDownload = downloadManager.getQueuedDownloadOrNull(update.episodeId) @@ -136,7 +197,6 @@ class UpdatesScreenModel( // <-- AM (FILE_SIZE) ) } - .toPersistentList() } fun updateLibrary(): Boolean { @@ -410,9 +470,44 @@ class UpdatesScreenModel( libraryPreferences.newUpdatesCount().set(0) } + private fun getUpdatesItemPreferenceFlow(): Flow { + return eu.kanade.core.util.combine( + updatesPreferences.filterDownloaded().changes(), + updatesPreferences.filterUnseen().changes(), + updatesPreferences.filterStarted().changes(), + updatesPreferences.filterBookmarked().changes(), + updatesPreferences.filterFillermarked().changes(), + updatesPreferences.filterExcludedScanlators().changes(), + ) { downloaded, unseen, started, bookmarked, fillermarked, excludedScanlators -> + ItemPreferences( + filterDownloaded = downloaded, + filterUnseen = unseen, + filterStarted = started, + filterBookmarked = bookmarked, + filterFillermarked = fillermarked, + filterExcludedScanlators = excludedScanlators, + ) + } + } + + fun showFilterDialog() { + mutableState.update { it.copy(dialog = Dialog.FilterSheet) } + } + + @Immutable + private data class ItemPreferences( + val filterDownloaded: TriState, + val filterUnseen: TriState, + val filterStarted: TriState, + val filterBookmarked: TriState, + val filterFillermarked: TriState, + val filterExcludedScanlators: Boolean, + ) + @Immutable data class State( val isLoading: Boolean = true, + val hasActiveFilters: Boolean = false, val items: PersistentList = persistentListOf(), val dialog: Dialog? = null, ) { @@ -436,6 +531,7 @@ class UpdatesScreenModel( sealed interface Dialog { data class DeleteConfirmation(val toDelete: List) : Dialog + data object FilterSheet : Dialog // AY --> data class ShowQualities( @@ -453,6 +549,14 @@ class UpdatesScreenModel( } } +private fun TriState.toBooleanOrNull(): Boolean? { + return when (this) { + TriState.DISABLED -> null + TriState.ENABLED_IS -> true + TriState.ENABLED_NOT -> false + } +} + @Immutable data class UpdatesItem( val update: UpdatesWithRelations, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesSettingsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesSettingsScreenModel.kt new file mode 100644 index 0000000000..1e909d4847 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesSettingsScreenModel.kt @@ -0,0 +1,20 @@ +package eu.kanade.tachiyomi.ui.updates + +import cafe.adriel.voyager.core.model.ScreenModel +import tachiyomi.core.common.preference.Preference +import tachiyomi.core.common.preference.TriState +import tachiyomi.core.common.preference.getAndSet +import tachiyomi.domain.updates.service.UpdatesPreferences +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class UpdatesSettingsScreenModel( + val updatesPreferences: UpdatesPreferences = Injekt.get(), +) : ScreenModel { + + fun toggleFilter(preference: (UpdatesPreferences) -> Preference) { + preference(updatesPreferences).getAndSet { + it.next() + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt index 2be74eca69..36c2285096 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt @@ -15,6 +15,7 @@ import eu.kanade.presentation.anime.EpisodeOptionsDialogScreen import eu.kanade.presentation.components.NavigatorAdaptiveSheet import eu.kanade.presentation.updates.UpdateScreen import eu.kanade.presentation.updates.UpdatesDeleteConfirmationDialog +import eu.kanade.presentation.updates.UpdatesFilterDialog import eu.kanade.tachiyomi.ui.anime.AnimeScreen import eu.kanade.tachiyomi.ui.home.HomeScreen import eu.kanade.tachiyomi.ui.main.MainActivity @@ -28,7 +29,11 @@ import uy.kohesive.injekt.injectLazy // AM (RECENTS_FILTER_CHIP) --> @Composable -fun AnimeUpdatesHalfTab(screenModel: UpdatesScreenModel, contentPadding: PaddingValues) { +fun AnimeUpdatesHalfTab( + screenModel: UpdatesScreenModel, + settingsScreenModel: UpdatesSettingsScreenModel, + contentPadding: PaddingValues, +) { val context = LocalContext.current val navigator = LocalNavigator.currentOrThrow val scope = rememberCoroutineScope() @@ -59,6 +64,12 @@ fun AnimeUpdatesHalfTab(screenModel: UpdatesScreenModel, contentPadding: Padding onConfirm = { screenModel.deleteEpisodes(dialog.toDelete) }, ) } + is UpdatesScreenModel.Dialog.FilterSheet -> { + UpdatesFilterDialog( + onDismissRequest = onDismissDialog, + screenModel = settingsScreenModel, + ) + } is UpdatesScreenModel.Dialog.ShowQualities -> { EpisodeOptionsDialogScreen.onDismissDialog = onDismissDialog NavigatorAdaptiveSheet( diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/view/EditTextPreferenceExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/view/EditTextPreferenceExtensions.kt index 4428fb9ae1..da3cb6d7dc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/view/EditTextPreferenceExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/view/EditTextPreferenceExtensions.kt @@ -5,6 +5,7 @@ package androidx.preference /** * Returns package-private [EditTextPreference.getOnBindEditTextListener] */ +@Suppress("EXTENSION_SHADOWED_BY_MEMBER") fun EditTextPreference.getOnBindEditTextListener(): EditTextPreference.OnBindEditTextListener? { return onBindEditTextListener } diff --git a/app/src/main/java/mihon/feature/migration/config/MigrationConfigScreen.kt b/app/src/main/java/mihon/feature/migration/config/MigrationConfigScreen.kt index 2cd031df16..6ab254864f 100644 --- a/app/src/main/java/mihon/feature/migration/config/MigrationConfigScreen.kt +++ b/app/src/main/java/mihon/feature/migration/config/MigrationConfigScreen.kt @@ -20,6 +20,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SmallExtendedFloatingActionButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -61,7 +62,6 @@ import tachiyomi.domain.source.service.SourceManager import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.FastScrollLazyColumn import tachiyomi.presentation.core.components.Pill -import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource @@ -143,7 +143,7 @@ class MigrationConfigScreen(private val animeIds: Collection) : Screen() { ) }, floatingActionButton = { - ExtendedFloatingActionButton( + SmallExtendedFloatingActionButton( text = { Text(text = stringResource(MR.strings.migrationConfigScreen_continueButtonText)) }, icon = { Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null) }, onClick = { diff --git a/app/src/main/java/mihon/feature/upcoming/components/calendar/CalendarHeader.kt b/app/src/main/java/mihon/feature/upcoming/components/calendar/CalendarHeader.kt index f5dbf5c485..b2cbbee6ee 100644 --- a/app/src/main/java/mihon/feature/upcoming/components/calendar/CalendarHeader.kt +++ b/app/src/main/java/mihon/feature/upcoming/components/calendar/CalendarHeader.kt @@ -54,9 +54,11 @@ fun CalenderHeader( } Row { IconButton(onClick = onPreviousClick) { + @Suppress("DEPRECATION") Icon(Icons.Default.KeyboardArrowLeft, stringResource(MR.strings.upcoming_calendar_prev)) } IconButton(onClick = onNextClick) { + @Suppress("DEPRECATION") Icon(Icons.Default.KeyboardArrowRight, stringResource(MR.strings.upcoming_calendar_next)) } } diff --git a/buildSrc/src/main/kotlin/mihon/buildlogic/ProjectExtensions.kt b/buildSrc/src/main/kotlin/mihon/buildlogic/ProjectExtensions.kt index 916ec38f42..a4f5577d3b 100644 --- a/buildSrc/src/main/kotlin/mihon/buildlogic/ProjectExtensions.kt +++ b/buildSrc/src/main/kotlin/mihon/buildlogic/ProjectExtensions.kt @@ -14,7 +14,6 @@ import org.gradle.kotlin.dsl.provideDelegate import org.gradle.kotlin.dsl.the import org.gradle.kotlin.dsl.withType import org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradlePluginExtension -import org.jetbrains.kotlin.compose.compiler.gradle.ComposeFeatureFlag import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.io.File @@ -42,7 +41,7 @@ internal fun Project.configureAndroid(commonExtension: CommonExtension<*, *, *, compilerOptions { jvmTarget.set(AndroidConfig.JvmTarget) freeCompilerArgs.addAll( - "-Xcontext-receivers", + "-Xcontext-parameters", "-opt-in=kotlin.RequiresOptIn", ) @@ -73,8 +72,6 @@ internal fun Project.configureCompose(commonExtension: CommonExtension<*, *, *, } extensions.configure { - featureFlags.set(setOf(ComposeFeatureFlag.OptimizeNonSkippingGroups)) - val enableMetrics = project.providers.gradleProperty("enableComposeCompilerMetrics").orNull.toBoolean() val enableReports = project.providers.gradleProperty("enableComposeCompilerReports").orNull.toBoolean() diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/OkHttpExtensions.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/OkHttpExtensions.kt index 2417f6b90c..0a860d0697 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/OkHttpExtensions.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/OkHttpExtensions.kt @@ -134,18 +134,18 @@ fun OkHttpClient.newCachelessCallWithProgress(request: Request, listener: Progre return progressClient.newCall(request) } -context(Json) +context(_: Json) inline fun Response.parseAs(): T { return decodeFromJsonResponse(serializer(), this) } -context(Json) +context(json: Json) fun decodeFromJsonResponse( deserializer: DeserializationStrategy, response: Response, ): T { return response.body.source().use { - decodeFromBufferedSource(deserializer, it) + json.decodeFromBufferedSource(deserializer, it) } } diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt index e978a52cf8..517679aca8 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt @@ -87,7 +87,7 @@ class CloudflareInterceptor( executor.execute { webview = createWebView(originalRequest) - webview?.webViewClient = object : WebViewClient() { + webview.webViewClient = object : WebViewClient() { override fun onPageFinished(view: WebView, url: String) { fun isCloudFlareBypassed(): Boolean { return cookieManager.get(origRequestUrl.toHttpUrl()) @@ -123,7 +123,7 @@ class CloudflareInterceptor( } } - webview?.loadUrl(origRequestUrl, headers) + webview.loadUrl(origRequestUrl, headers) } latch.awaitFor30Seconds() diff --git a/data/src/main/java/tachiyomi/data/updates/UpdatesRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/updates/UpdatesRepositoryImpl.kt index df17aeb3a5..9c5eaabe88 100644 --- a/data/src/main/java/tachiyomi/data/updates/UpdatesRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/updates/UpdatesRepositoryImpl.kt @@ -1,6 +1,7 @@ package tachiyomi.data.updates import kotlinx.coroutines.flow.Flow +import tachiyomi.core.common.util.lang.toLong import tachiyomi.data.DatabaseHandler import tachiyomi.domain.anime.model.AnimeCover import tachiyomi.domain.updates.model.UpdatesWithRelations @@ -25,9 +26,27 @@ class UpdatesRepositoryImpl( } } - override fun subscribeAll(after: Long, limit: Long): Flow> { + override fun subscribeAll( + after: Long, + limit: Long, + unseen: Boolean?, + started: Boolean?, + bookmarked: Boolean?, + fillermarked: Boolean?, + hideExcludedScanlators: Boolean, + ): Flow> { return databaseHandler.subscribeToList { - updatesViewQueries.getRecentUpdates(after, limit, ::mapUpdatesWithRelations) + updatesViewQueries.getRecentUpdatesWithFilters( + after = after, + limit = limit, + // invert because unseen in Kotlin -> seen column in SQL + seen = unseen?.let { !it }, + started = started?.toLong(), + bookmarked = bookmarked, + fillermarked = fillermarked, + hideExcludedScanlators = hideExcludedScanlators.toLong(), + mapper = ::mapUpdatesWithRelations, + ) } } @@ -68,6 +87,7 @@ class UpdatesRepositoryImpl( coverLastModified: Long, dateUpload: Long, dateFetch: Long, + excludedScanlator: String?, ): UpdatesWithRelations = UpdatesWithRelations( animeId = animeId, // AM (CUSTOM_INFORMATION) --> diff --git a/data/src/main/sqldelight/tachiyomi/migrations/137.sqm b/data/src/main/sqldelight/tachiyomi/migrations/137.sqm new file mode 100644 index 0000000000..1770301c13 --- /dev/null +++ b/data/src/main/sqldelight/tachiyomi/migrations/137.sqm @@ -0,0 +1,35 @@ +-- Add excluded_scanlators to updatesView +DROP VIEW IF EXISTS updatesView; + +CREATE VIEW updatesView AS +SELECT + animes._id AS animeId, + animes.title AS animeTitle, + episodes._id AS episodeId, + episodes.name AS episodeName, + episodes.scanlator, + episodes.url AS episodeUrl, + episodes.seen, + episodes.bookmark, + -- AY --> + episodes.fillermark, + -- <-- AY + episodes.last_second_seen, + -- AY --> + episodes.total_seconds AS totalSeconds, + -- <-- AY + animes.source, + animes.favorite, + animes.thumbnail_url AS thumbnailUrl, + animes.cover_last_modified AS coverLastModified, + episodes.date_upload AS dateUpload, + episodes.date_fetch AS datefetch, + excluded_scanlators.scanlator AS excludedScanlator +FROM animes JOIN episodes +ON animes._id = episodes.anime_id +LEFT JOIN excluded_scanlators +ON animes._id = excluded_scanlators.anime_id +AND episodes.scanlator = excluded_scanlators.scanlator +WHERE favorite = 1 +AND date_fetch > date_added +ORDER BY date_fetch DESC; diff --git a/data/src/main/sqldelight/tachiyomi/view/updatesView.sq b/data/src/main/sqldelight/tachiyomi/view/updatesView.sq index bb4248b60f..d8017e70fd 100644 --- a/data/src/main/sqldelight/tachiyomi/view/updatesView.sq +++ b/data/src/main/sqldelight/tachiyomi/view/updatesView.sq @@ -20,9 +20,13 @@ SELECT animes.thumbnail_url AS thumbnailUrl, animes.cover_last_modified AS coverLastModified, episodes.date_upload AS dateUpload, - episodes.date_fetch AS datefetch + episodes.date_fetch AS datefetch, + excluded_scanlators.scanlator AS excludedScanlator FROM animes JOIN episodes ON animes._id = episodes.anime_id +LEFT JOIN excluded_scanlators +ON animes._id = excluded_scanlators.anime_id +AND episodes.scanlator = excluded_scanlators.scanlator WHERE favorite = 1 AND date_fetch > date_added ORDER BY date_fetch DESC; @@ -33,6 +37,24 @@ FROM updatesView WHERE dateUpload > :after LIMIT :limit; +getRecentUpdatesWithFilters: +SELECT * +FROM updatesView +WHERE dateUpload > :after +AND (:seen IS NULL OR seen = :seen) +-- Started means some progress but not finished, Seen means finished episode, thus: +AND ( + :started IS NULL + OR (:started = 1 AND last_second_seen > 0 AND seen = 0) + OR (:started = 0 AND last_second_seen = 0 AND seen = 0) +) +AND (:bookmarked IS NULL OR bookmark = :bookmarked) +AND (:fillermarked IS NULL OR fillermark = :fillermarked) +AND ( + (excludedScanlator IS NULL OR :hideExcludedScanlators = 0) +) +LIMIT :limit; + getUpdatesBySeenStatus: SELECT * FROM updatesView diff --git a/domain/src/main/java/tachiyomi/domain/updates/interactor/GetUpdates.kt b/domain/src/main/java/tachiyomi/domain/updates/interactor/GetUpdates.kt index e9f8152ebd..59269bd32c 100644 --- a/domain/src/main/java/tachiyomi/domain/updates/interactor/GetUpdates.kt +++ b/domain/src/main/java/tachiyomi/domain/updates/interactor/GetUpdates.kt @@ -13,8 +13,23 @@ class GetUpdates( return repository.awaitWithSeen(seen, after, limit = 500) } - fun subscribe(instant: Instant): Flow> { - return repository.subscribeAll(instant.toEpochMilli(), limit = 500) + fun subscribe( + instant: Instant, + unseen: Boolean?, + started: Boolean?, + bookmarked: Boolean?, + fillermarked: Boolean?, + hideExcludedScanlators: Boolean, + ): Flow> { + return repository.subscribeAll( + instant.toEpochMilli(), + limit = 500, + unseen = unseen, + started = started, + bookmarked = bookmarked, + fillermarked = fillermarked, + hideExcludedScanlators = hideExcludedScanlators, + ) } fun subscribe(seen: Boolean, after: Long): Flow> { diff --git a/domain/src/main/java/tachiyomi/domain/updates/repository/UpdatesRepository.kt b/domain/src/main/java/tachiyomi/domain/updates/repository/UpdatesRepository.kt index 528e937dd0..435d56f130 100644 --- a/domain/src/main/java/tachiyomi/domain/updates/repository/UpdatesRepository.kt +++ b/domain/src/main/java/tachiyomi/domain/updates/repository/UpdatesRepository.kt @@ -7,7 +7,15 @@ interface UpdatesRepository { suspend fun awaitWithSeen(seen: Boolean, after: Long, limit: Long): List - fun subscribeAll(after: Long, limit: Long): Flow> + fun subscribeAll( + after: Long, + limit: Long, + unseen: Boolean?, + started: Boolean?, + bookmarked: Boolean?, + fillermarked: Boolean?, + hideExcludedScanlators: Boolean, + ): Flow> fun subscribeWithSeen(seen: Boolean, after: Long, limit: Long): Flow> } diff --git a/domain/src/main/java/tachiyomi/domain/updates/service/UpdatesPreferences.kt b/domain/src/main/java/tachiyomi/domain/updates/service/UpdatesPreferences.kt new file mode 100644 index 0000000000..468b6db486 --- /dev/null +++ b/domain/src/main/java/tachiyomi/domain/updates/service/UpdatesPreferences.kt @@ -0,0 +1,40 @@ +package tachiyomi.domain.updates.service + +import tachiyomi.core.common.preference.PreferenceStore +import tachiyomi.core.common.preference.TriState +import tachiyomi.core.common.preference.getEnum + +class UpdatesPreferences( + private val preferenceStore: PreferenceStore, +) { + + fun filterDownloaded() = preferenceStore.getEnum( + "pref_filter_updates_downloaded", + TriState.DISABLED, + ) + + fun filterUnseen() = preferenceStore.getEnum( + "pref_filter_updates_unseen", + TriState.DISABLED, + ) + + fun filterStarted() = preferenceStore.getEnum( + "pref_filter_updates_started", + TriState.DISABLED, + ) + + fun filterBookmarked() = preferenceStore.getEnum( + "pref_filter_updates_bookmarked", + TriState.DISABLED, + ) + + fun filterFillermarked() = preferenceStore.getEnum( + "pref_filter_updates_fillermarked", + TriState.DISABLED, + ) + + fun filterExcludedScanlators() = preferenceStore.getBoolean( + "pref_filter_updates_hide_excluded_scanlators", + false, + ) +} diff --git a/gradle/androidx.versions.toml b/gradle/androidx.versions.toml index c77ce2f3d8..aeb199ce7a 100644 --- a/gradle/androidx.versions.toml +++ b/gradle/androidx.versions.toml @@ -1,7 +1,7 @@ [versions] agp_version = "8.13.2" lifecycle_version = "2.10.0" -paging_version = "3.3.6" +paging_version = "3.4.0" interpolator_version = "1.0.0" [libraries] @@ -21,7 +21,7 @@ lifecycle-common = { module = "androidx.lifecycle:lifecycle-common", version.ref lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle_version" } lifecycle-runtimektx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle_version" } -workmanager = "androidx.work:work-runtime:2.11.0" +workmanager = "androidx.work:work-runtime:2.11.1" paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "paging_version" } paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging_version" } diff --git a/gradle/compose.versions.toml b/gradle/compose.versions.toml index ccd2659d00..87c0cefade 100644 --- a/gradle/compose.versions.toml +++ b/gradle/compose.versions.toml @@ -1,8 +1,8 @@ [versions] -compose-bom = "2025.12.01" +compose-bom = "2026.01.01" [libraries] -activity = "androidx.activity:activity-compose:1.12.2" +activity = "androidx.activity:activity-compose:1.12.3" bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" } foundation = { module = "androidx.compose.foundation:foundation" } animation = { module = "androidx.compose.animation:animation" } diff --git a/gradle/kotlinx.versions.toml b/gradle/kotlinx.versions.toml index 81142d4547..c33ba7668f 100644 --- a/gradle/kotlinx.versions.toml +++ b/gradle/kotlinx.versions.toml @@ -1,6 +1,6 @@ [versions] kotlin_version = "2.3.0" -serialization_version = "1.9.0" +serialization_version = "1.10.0" xml_serialization_version = "0.91.3" [libraries] diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 26f4ca4c1b..41efe2a52f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,11 +7,12 @@ shizuku_version = "13.1.5" sqldelight = "2.2.1" sqlite = "2.6.2" voyager = "1.1.0-beta03" -spotless = "8.1.0" +spotless = "8.2.1" ktlint-core = "1.8.0" -firebase-bom = "34.7.0" -markdown = "0.39.0" -junit = "6.0.1" +firebase-bom = "34.8.0" +markdown = "0.39.2" +junit = "6.0.2" +materialKolor = "5.0.0-alpha06" [libraries] desugar = "com.android.tools:desugar_jdk_libs:2.1.5" @@ -29,7 +30,7 @@ conscrypt-android = "org.conscrypt:conscrypt-android:2.5.3" quickjs-android = { group = "com.github.zhanghai.quickjs-java", name = "quickjs-android", version = "547f5b1597" } -jsoup = "org.jsoup:jsoup:1.21.2" +jsoup = "org.jsoup:jsoup:1.22.1" disklrucache = "com.jakewharton:disklrucache:2.0.2" unifile = "com.github.tachiyomiorg:unifile:e0def6b3dc" @@ -90,8 +91,8 @@ sqldelight-dialects-sql = { module = "app.cash.sqldelight:sqlite-3-38-dialect", junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" } -kotest-assertions = "io.kotest:kotest-assertions-core:6.0.7" -mockk = "io.mockk:mockk:1.14.7" +kotest-assertions = "io.kotest:kotest-assertions-core:6.1.3" +mockk = "io.mockk:mockk:1.14.9" voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" } voyager-screenmodel = { module = "cafe.adriel.voyager:voyager-screenmodel", version.ref = "voyager" } @@ -106,6 +107,8 @@ markdown-coil = { module = "com.mikepenz:multiplatform-markdown-renderer-coil3", stringSimilarity = { module = "com.aallam.similarity:string-similarity-kotlin", version = "0.1.0" } +materialKolor = { module = "com.materialkolor:material-kolor", version.ref = "materialKolor" } + [plugins] google-services = { id = "com.google.gms.google-services", version = "4.4.4" } aboutLibraries = { id = "com.mikepenz.aboutlibraries.plugin.android", version.ref = "aboutlib_version" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d4081da476..aaaabb3cb9 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/i18n-animiru/src/commonMain/moko-resources/base/strings.xml b/i18n-animiru/src/commonMain/moko-resources/base/strings.xml index 06732059e5..51542a398c 100644 --- a/i18n-animiru/src/commonMain/moko-resources/base/strings.xml +++ b/i18n-animiru/src/commonMain/moko-resources/base/strings.xml @@ -58,6 +58,7 @@ No episodes found, this entry cannot be used for migration No seasons found, this entry cannot be used for migration Ensures compatibility with certain storage media that don't support Unicode. When this is enabled, you'll need to manually rename source and anime folders by replacing non-ASCII characters with their lowercase UTF-8 hexadecimal representations. Episode files don't need to be renamed. + Unseen Mismatched fetch type diff --git a/i18n/src/commonMain/moko-resources/ar/strings.xml b/i18n/src/commonMain/moko-resources/ar/strings.xml index 92daeeed5a..32186ea609 100644 --- a/i18n/src/commonMain/moko-resources/ar/strings.xml +++ b/i18n/src/commonMain/moko-resources/ar/strings.xml @@ -3,9 +3,9 @@ Ø§Ų„Ø§ØŗŲ… Ø§Ų„ŲØĻاØĒ ØĨØ¯ØŽØ§Ų„Ø§ØĒ Ø§Ų„Ų…ŲƒØĒب؊ - Ø§Ų„ŲØĩŲˆŲ„ - Ø§Ų„ØĒØšŲ‚Ø¨ - Ø§Ų„ØĒØ§ØąŲŠØŽ + ؁ØĩŲˆŲ„ + ØĒØšŲ‚Ø¨ + ØĒØ§ØąŲŠØŽ اīģšØšØ¯Ø§Ø¯Ø§ØĒ Ų‚Ø§ØĻŲ…ØŠ Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„Ø§ØĒ Ø§Ų„Ų…ŲƒØĒب؊ @@ -702,7 +702,7 @@ Ų‡Ø°Ø§ ŲŠØ˛ŲŠŲ„ Ø§Ų„ØĒØĒبؚ Ų…Ø­Ų„ŲŠŲ‘Ų‹Ø§. Ø­ØŗŲ†Ų‹Ø§ ŲˆŲƒØ°Ų„Ųƒ ØŖØ˛Ų„Ų‡ Ų…Ų† %s - Ø§Ø­Ø°Ų Ų…Ø§ Ų†ŲØ˛Ų‘ŲŲ„ + Ø­Ø°Ų Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„Ø§ØĒ ØĒŲØ˛Ø§Ų…Ų† Ø§Ų„Ų…ŲƒØĒب؊ Ø§Ų†ØĒŲ‡ØĒ Ų…Ø˛Ø§Ų…Ų†ØŠ Ø§Ų„Ų…ŲƒØĒب؊ ŲˆŲØŦدØĒ Ų†ØĒاØĻØŦ @@ -732,7 +732,7 @@ Ų†ŲŲ‘ŲØ° ØĩŲŲ‘ŲØą Ų„Ų… ŲŠŲØšØĢŲŽØą ØšŲ„Ų‰ Ų…ØĒØąØŦŲ…ŲŠŲ† - Ø§Ų„Ų…ØĒØąØŦŲ… + ØŗŲƒØ§Ų†Ų„ŲŠØĒŲˆØą احØŦب بؚØļ Ø§Ų„Ų…ØĒØąØŦŲ…ŲŠŲ† ØŽŲŠØ§ØąØ§ØĒ ØŖŲƒØĢØą Ų…Ø­Ø¯Ų‘ŲŽØ¯ diff --git a/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml index 6092636a07..2db93d49dc 100644 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -607,7 +607,7 @@ Cookies cleared Reindex downloads Force app to recheck downloaded chapters - Downloads index invalidated + Recreating download index Clear database Delete history for entries that are not saved in your library %1$d non-library entries in database @@ -854,6 +854,7 @@ Just now Never View Upcoming Updates + Filter excluded scanlators Upcoming Guide diff --git a/i18n/src/commonMain/moko-resources/bn/strings.xml b/i18n/src/commonMain/moko-resources/bn/strings.xml index 8053555aaf..a297619430 100644 --- a/i18n/src/commonMain/moko-resources/bn/strings.xml +++ b/i18n/src/commonMain/moko-resources/bn/strings.xml @@ -10,7 +10,7 @@ āĻĄāĻžāωāύāϞ⧋āĻĄ āϏāĻžāϰāĻŋ āϞāĻžāχāĻŦā§āϰ⧇āϰāĻŋ āχāϤāĻŋāĻšāĻžāϏ - āĻšāĻžāϞāύāĻžāĻ—āĻžāĻĻāϏāĻŽā§‚āĻš + āφāĻĒāĻĄā§‡āϟ āĻŦā§āϝāĻžāĻ•āφāĻĒ āĻāĻŦāĻ‚ āĻĒ⧁āύāϰ⧁āĻĻā§āϧāĻžāϰ āϏ⧇āϟāĻŋāĻ‚āϏ āĻŦāĻŋāĻļā§‹āϧāύ @@ -30,20 +30,20 @@ āĻŦ⧁āĻ•āĻŽāĻžāĻ°ā§āĻ• āĻ…āĻ§ā§āϝāĻžāϝāĻŧ āĻŦ⧁āĻ•āĻŽāĻžāĻ°ā§āĻ• āϏāϰāĻžāύ āĻŽā§āϛ⧁āύ - āϏāĻ‚āĻ—ā§āϰāĻšāĻļāĻžāϞāĻž āĻšāĻžāϞāύāĻžāĻ—āĻžāĻĻ + āϞāĻžāχāĻŦā§āϰ⧇āϰāĻŋ āφāĻĒāĻĄā§‡āϟ āϏāĻŽā§āĻĒāĻžāĻĻāύ āĻ•āϰ⧁āύ āϝ⧋āĻ— āĻ•āϰ⧁āύ - āĻŦāĻŋāĻ­āĻžāĻ— āϝ⧋āĻ— āĻ•āϰ⧁āύ - āĻŦāĻŋāĻ­āĻžāĻ— āϏāĻŽā§āĻĒāĻžāĻĻāύ āĻ•āϰ⧁āύ - āĻŦāĻŋāĻ­āĻžāϗ⧇āϰ āύāϤ⧁āύ āύāĻžāĻŽāĻ•āϰāĻŖ āĻ•āϰ⧁āύ - āĻŦāĻŋāĻ­āĻžāĻ— āύāĻŋāĻ°ā§āĻŦāĻžāϚāύ āĻ•āϰ⧁āύ + āĻ•ā§āϝāĻžāϟāĻžāĻ—āϰāĻŋ āĻ…ā§āϝāĻžāĻĄ + āĻ•ā§āϝāĻžāϟāĻžāĻ—āϰāĻŋ āĻāĻĄāĻŋāϟ + āĻ•ā§āϝāĻžāϟāĻžāĻ—āϰāĻŋāϰ āύāĻžāĻŽ āĻĒāϰāĻŋāĻŦāĻ°ā§āϤāύ + āĻ•ā§āϝāĻžāϟāĻžāĻ—āϰāĻŋ āϏ⧇āϟ āĻ•āϰ⧁āύ āĻŽā§‹ā§œāĻ• āϏāĻŽā§āĻĒāĻžāĻĻāύāĻž āĻ•āϰ⧁āύ āĻŦāĻŋāϰāϤāĻŋ āĻĻāĻŋāύ āĻĒā§‚āĻ°ā§āĻŦāĻŦāĻ°ā§āϤ⧀ āĻ…āĻ§ā§āϝāĻžā§Ÿ āĻĒāϰāĻŦāĻ°ā§āϤ⧀ āφāĻ§ā§āϝāĻžā§Ÿ āĻĒ⧁āύāϰāĻžāϝāĻŧ āĻšā§‡āĻˇā§āϟāĻž āĻ•āϰ⧁āύ āϏāϰāĻžāύ - āĻĒ⧁āύāϰāĻžāϰāĻŽā§āĻ­ āĻ•āϰ⧁āύ + āϰāĻŋāϜāĻŋāωāĻŽ āĻŦā§āϰāĻžāωāϜāĻžāϰ⧇ āϖ⧁āϞ⧁āύ āĻĒā§āϰāĻĻāĻ°ā§āĻļāύ⧇āϰ āϧāϰāύ āĻĒā§āϰāĻĻāĻ°ā§āĻļāύ @@ -53,7 +53,7 @@ āĻŦāĻžāϤāĻŋāϞ āϏāĻžāϜāĻžāύ āχāĻ¨ā§āϏāϟāϞ āĻ•āϰ⧁āύ - āĻļā§‡ā§ŸāĻžāϰ āĻ•āϰ⧁āύ + āĻļā§‡ā§ŸāĻžāϰ āϏāĻ‚āϰāĻ•ā§āώāĻŖ āĻ•āϰ⧁āύ āĻĒ⧁āύ:āĻ¸ā§āĻĨāĻžāĻĒāύ āĻĒā§‚āĻ°ā§āĻŦāĻžāĻŦāĻ¸ā§āĻĨāĻžāϝāĻŧ āĻĢāĻŋāϰ⧁āύ @@ -62,10 +62,10 @@ āϞ⧋āĻĄ āĻšāĻšā§āϛ⧇â€Ļ āĻ…ā§āϝāĻžāĻĒāϟāĻŋ āĻ…āύ⧁āĻĒāϞāĻŦā§āϧ āϏāĻžāϧāĻžāϰāĻŖ - āĻĒāĻžāĻ āĻ• - āĻĄāĻžāωāύāϞ⧋āĻĄāϗ⧁āϞ⧋ - āĻ…āύ⧁āϏāϰāĻŋāϤ - āĻ…āĻ—ā§āϰāĻŦāĻ°ā§āϤ⧀ + āϰāĻŋāĻĄāĻžāϰ + āĻĄāĻžāωāύāϞ⧋āĻĄāϏ + āĻŸā§āĻ°ā§āϝāĻžāĻ•āĻŋāĻ‚ + āĻ…ā§āϝāĻžāĻĄāĻ­āĻžāĻ¨ā§āϏāĻĄ āϏāĻŽā§āĻŦāĻ¨ā§āϧ⧇ āĻĒā§āϰāϤāĻŋ āϏāĻžāϰāĻŋāϤ⧇ āφāχāĻŸā§‡āĻŽ āĻĒā§āϰāϤāĻŋāĻ•ā§ƒāϤāĻŋ @@ -98,10 +98,10 @@ āϏāĻžāĻĻāĻž āĻ•āĻžāϞ⧋ āĻĒā§āϰāĻ•ā§ƒāϤ āĻĒāĻ āύ āϧāϰāύ - āĻŦāĻžāĻŽ āĻĨ⧇āϕ⧇ āĻĄāĻžāύ⧇ - āĻĄāĻžāύ āĻĨ⧇āϕ⧇ āĻŦāĻžāĻŽā§‡ - āωāĻ˛ā§āϞāĻŽā§āĻŦ - āĻ“ā§Ÿā§‡āĻŦāϟ⧁āύ + āĻŦāĻžāĻŽ āĻĨ⧇āϕ⧇ āĻĄāĻžāύ⧇ (āĻĒ⧇āϜāĻĄ) + āĻĄāĻžāύ āĻĨ⧇āϕ⧇ āĻŦāĻžāĻŽā§‡ (āĻĒ⧇āϜāĻĄ) + āωāĻ˛ā§āϞāĻŽā§āĻŦāĻ­āĻžāĻŦ⧇ (āĻĒ⧇āϜāĻĄ) + āϞāĻ‚ āĻ¸ā§āĻŸā§āϰāĻŋāĻĒ āĻ¸ā§āϕ⧇āϞ āϧāϰāĻŖ āϏāĻŽā§āĻĒā§‚āĻ°ā§āĻŖ āĻĒāĻ°ā§āĻĻāĻž āĻœā§ā§œā§‡ āĻĒā§āϰāϏāĻžāϰāύ @@ -114,7 +114,7 @@ āĻŦāĻžāĻŽā§‡ āĻĄāĻžāύ⧇ āĻŽāĻžāĻā§‡ - āĻĒā§āϰāĻ•ā§ƒāϤ āĻ˜ā§‚āĻ°ā§āĻŖāύ⧇āϰ āϧāϰāύ + āĻĄāĻŋāĻĢāĻ˛ā§āϟ āĻ˜ā§‚āĻ°ā§āĻŖāύ āĻŽā§āĻ•ā§āϤ āϞāĻ• āĻ•āϰāĻž āĻĒā§‹āĻ°ā§āĻŸā§āϰ⧇āϟ āϞāĻ• āĻ•āϰāĻž āĻ˛ā§āϝāĻžāĻ¨ā§āĻĄāĻ¸ā§āϕ⧇āĻĒ @@ -151,7 +151,7 @@ āĻĄāĻžāϟāĻžāĻŦ⧇āϜ āĻĒāϰāĻŋāĻˇā§āĻ•āĻžāϰ āĻ•āϰ⧁āύ āφāĻĒāύāĻžāϰ āϏāĻ‚āĻ—ā§āϰāĻšāĻļāĻžāϞāĻžāϤ⧇ āϝ⧇āϏāĻŦ āĻŽāĻžāĻ‚āĻ—āĻž āϏāĻ‚āϰāĻ•ā§āώāĻŋāϤ āύ⧇āχ āϏ⧇āϗ⧁āϞ⧋āϰ āχāϤāĻŋāĻšāĻžāϏ āĻŽā§āϛ⧇ āĻĢ⧇āϞ⧁āύ āĻāĻ¨ā§āĻŸā§āϰāĻŋāϗ⧁āϞ⧋ āĻŽā§āϛ⧇ āĻĢ⧇āϞāĻž āĻšā§Ÿā§‡āϛ⧇ - āϏāĻ‚āĻ¸ā§āĻ•āϰāĻŖ + āĻ­āĻžāĻ°ā§āϏāύ āĻ•ā§āĻ°â€ā§āϝāĻžāĻļ⧇āϰ āĻĒā§āϰāϤāĻŋāĻŦ⧇āĻĻāύ āĻĒāĻžāĻ āĻžāύ āĻŦāĻžāĻ— āĻ āĻŋāĻ• āĻ•āϰāĻžāϰ āϜāĻ¨ā§āϝ āϏāĻžāĻšāĻžāĻ¯ā§āϝ āĻ•āϰ⧁āύāĨ¤ āϕ⧋āύ āϏāĻ‚āĻŦ⧇āĻĻāύāĻļā§€āϞ āϤāĻĨā§āϝ āĻĒāĻžāĻ āĻžāύ⧋ āĻšāĻŦ⧇ āύāĻž āĻĒā§āϰāĻŦ⧇āĻļ āĻ•āϰ⧁āύ %1$sāϤ⧇ @@ -162,10 +162,10 @@ āĻ…āϜāĻžāύāĻž āĻ¤ā§āϰ⧁āϟāĻŋ āĻŦāĻŋāĻ­āĻžāϗ⧇āϰ āĻšāĻžāϞāύāĻžāĻ—āĻžāĻĻ āĻšāĻšā§āϛ⧇ āφāϰ āϕ⧋āύāĻ“ āĻĢāϞāĻžāĻĢāϞ āύ⧇āχ - āĻ¸ā§āĻĨāĻžāύ⧀āϝāĻŧ āĻ‰ā§ŽāϏ + āϞ⧋āĻ•āĻžāϞ āϏ⧋āĻ°ā§āϏ āĻ…āĻ¨ā§āϝāĻžāĻ¨ā§āϝ āϏāĻžāĻ°ā§āĻŦāϜāύ⧀āύ āĻ–ā§‹āρāϜâ€Ļ - āϏāĻ°ā§āĻŦāĻļ⧇āώ + āϞ⧇āĻŸā§‡āĻ¸ā§āϟ āĻ…āύ⧁āϏāĻ¨ā§āϧāĻžāύ āϚāϞāĻŽāĻžāύ āĻ…āϜāĻžāύāĻž @@ -229,15 +229,15 @@ āϕ⧋āύ āύ⧇āϟāĻ“ā§ŸāĻžāĻ°ā§āĻ• āϏāĻ‚āϝ⧋āĻ— āϖ⧁āρāĻœā§‡ āĻĒāĻžāĻ“ā§ŸāĻž āϝāĻžā§ŸāύāĻŋ āĻĄāĻžāωāύāϞ⧋āĻĄ āĻŦāĻŋāϰāϤāĻŋ āϏāĻžāϧāĻžāϰāĻŖ - āĻ¸ā§āĻĨāĻžāύāĻžāĻ¨ā§āϤāϰ āĻ•āϰ⧁āύ - āĻāĻ•ā§āϏāĻŸā§‡āύāĻļāύ āϗ⧁āϞ⧋ + āĻŽāĻžāχāĻ—ā§āϰ⧇āϟ + āĻāĻ•ā§āϏāĻŸā§‡āύāĻļāύ āĻāĻ•ā§āϏāĻŸā§‡āύāĻļāύ⧇āϰ āĻŦāĻŋāĻŦāϰāύ āĻšāĻžāϞāύāĻžāĻ—āĻžāĻĻ āχāĻ¨ā§āϏāϟāϞ āĻĒā§āϰāĻ•ā§āϰāĻŋ⧟āĻžāϧ⧀āύ āĻĄāĻžāωāύāϞ⧋āĻĄ āĻšāĻšā§āϛ⧇ āχāĻ¨ā§āϏāϟāϞ āĻšāĻšā§āϛ⧇ - āχāĻ¨ā§āϏāϟāϞ āĻšā§Ÿā§‡āϛ⧇ + āχāύāĻ¸ā§āϟāϞ āĻ•āϰāĻž āĻŦāĻŋāĻļā§āĻŦāĻžāϏ āĻ…āύāĻŋāĻ°ā§āĻ­āϰāϝ⧋āĻ—ā§āϝ āφāύ āχāĻ¨ā§āϏāϟāϞ @@ -252,7 +252,7 @@ āĻ•ā§āϞāĻŋāĻĒāĻŦā§‹āĻ°ā§āĻĄā§‡ āĻ•āĻĒāĻŋ āĻšā§Ÿā§‡āϛ⧇: \n%1$s āϝ⧋āĻ— āĻ•āϰāĻžāϰ āϜāĻ¨ā§āϝ āωāĻĒāĻžāĻ¤ā§āϤ āύāĻŋāĻ°ā§āĻŦāĻžāϚāύ āĻ•āϰ⧁āύ - āĻ¸ā§āĻĨāĻžāύāĻžāĻ¨ā§āϤāϰ + āĻŽāĻžāχāĻ—ā§āϰ⧇āϟ āĻ…āύ⧁āϞāĻŋāĻĒāĻŋ āĻĒ⧇āϜāĻĄ āĻ‰ā§ŽāϏ āχāĻ¨ā§āϏāϟāϞ āĻ•āϰāĻž āύ⧇āχ: %1$s @@ -278,23 +278,23 @@ āϗ⧁āĻŖ āϏāĻžāĻšāĻžāĻ¯ā§āϝ āϕ⧋āύ āĻĢāϞāĻžāĻĢāϞ āĻĒāĻžāĻ“āϝāĻŧāĻž āϝāĻžāϝāĻŧāύāĻŋ - āĻ¸ā§āĻĨāĻžāύāĻžāĻ¨ā§āϤāϰ āĻ•āϰāϤ⧇ āĻāĻ•āϟāĻŋ āĻ‰ā§ŽāϏ āύāĻŋāĻ°ā§āĻŦāĻžāϚāύ āĻ•āϰ⧁āύ + āĻŽāĻžāχāĻ—ā§āϰ⧇āϟ āĻ•āϰāĻžāϰ āϜāĻ¨ā§āϝ āĻāĻ•āϟāĻŋ āϏ⧋āĻ°ā§āϏ āϏāĻŋāϞ⧇āĻ•ā§āϟ āĻ•āϰ⧁āύ āĻĒ⧇āĻ›āύ⧇ āĻ…āĻ—ā§āϰāĻŦāĻ°ā§āϤ⧀ - āϰ⧇āĻĢā§āϰ⧇āϏ āĻ•āϰ⧁āύ - āϏāĻ‚āĻ—ā§āϰāĻšāĻļāĻžāϞāĻž + āϰāĻŋāĻĢā§āϰ⧇āĻļ + āϞāĻžāχāĻŦā§āϰ⧇āϰāĻŋ āĻ…āϚāϞ āĻāχ āĻāĻ•ā§āϏāĻŸā§‡āύāĻļāύ āφāϰ āωāĻĒāϞāĻŦā§āϧ āύ⧟āĨ¤ āχāĻŽā§‡āϞ āĻ āĻŋāĻ•āĻžāύāĻž āϞāĻžāχāĻŦā§āϰ⧇āϰāĻŋāϤ⧇ āφāϛ⧇ - āϞāĻžāχāĻŦā§āϰ⧇āϰāĻŋāϤ⧇ āϰāĻžāĻ–ā§‹ + āϞāĻžāχāĻŦā§āϰ⧇āϰāĻŋāϤ⧇ āϝ⧁āĻ•ā§āϤ āĻ•āϰ⧁āύ āφāϰāĻ“ āĻ•āĻŽ āωāĻ˛ā§āĻŸā§‹ āύāĻŋāĻ°ā§āĻŦāĻžāϚāύ āĻ•āϰ⧁āύ āϤāĻžāϰāĻŋāϖ⧇ āϝ⧋āĻ—āĻ•ā§ƒāϤ āϏāĻ°ā§āĻŦāĻļ⧇āώ āĻ…āĻ§ā§āϝāĻžā§Ÿ āϤāĻžāϞāĻŋāĻ•āĻž - āĻ‰ā§ŽāϏ āϏāĻŽā§‚āĻš + āϏ⧋āĻ°ā§āϏ āφāϰāĻ“ āĻĒā§āϰāĻĻāĻ°ā§āĻļāύ āĻāϟāĻŋ āĻ…ā§āϝāĻžāĻĒ⧇āϰ āĻŽāĻ§ā§āϝ⧇ āĻĒā§āϰāĻžāĻĒā§āϤāĻŦāϝāĻŧāĻ¸ā§āĻ•āĻĻ⧇āϰ (18+) āĻŦāĻŋāώāϝāĻŧāĻŦāĻ¸ā§āϤ⧁ āϏāĻžāϰāĻĢ⧇āϏ āĻ•āϰāĻž āĻĨ⧇āϕ⧇ āĻ…āύāĻžāύ⧁āĻˇā§āĻ āĻžāύāĻŋāĻ• āĻŦāĻž āϏāĻŽā§āĻ­āĻžāĻŦā§āϝ āϭ⧁āϞāĻ­āĻžāĻŦ⧇ āĻĒāϤāĻžāĻ•āĻžāĻ™ā§āĻ•āĻŋāϤ āĻāĻ•ā§āϏāĻŸā§‡āύāĻļāĻžāύāϗ⧁āϞāĻŋāϕ⧇ āĻŦāĻžāϧāĻž āĻĻ⧇āϝāĻŧ āύāĻžāĨ¤ @@ -308,7 +308,7 @@ āϤāĻžāϰāĻŋāϖ⧇āϰ āϰ⧀āϤāĻŋ āĻĄāĻžāĻ°ā§āĻ• āϞāĻžāχāϟ - āϏāĻŋāĻ¸ā§āĻŸā§‡āĻŽāϕ⧇ āĻ…āύ⧁āϏāϰāĻŖ āĻ•āϰ⧁āύ + āϏāĻŋāĻ¸ā§āĻŸā§‡āĻŽ āĻĨāĻŋāĻŽ āύ⧀āĻšā§‡ āϏāϰāĻžāύ āĻļā§€āĻ°ā§āώ⧇ āϏāϰāĻžāύ @@ -318,14 +318,14 @@ āφāϞāĻ—āĻž āĻ•āϰ⧁āύ āφāϟāĻ•āĻžāύ āĻ…āĻ•ā§āώāĻŽ - āĻŦāĻŋāĻ­āĻžāϗ⧇āϰ āĻŸā§āϝāĻžāĻŦāϗ⧁āϞāĻŋ āĻĻ⧇āϖ⧁āύ + āĻ•ā§āϝāĻžāϟāĻžāĻ—āϰāĻŋ āĻŸā§āϝāĻžāĻŦāϗ⧁āϞāĻŋ āĻĻ⧇āĻ–āĻžāύ āφāϰāĻžāĻŽāĻĒā§āϰāĻĻ āĻ—ā§āϰāĻŋāĻĄ - āĻ¸ā§āĻĨāĻžāύāĻžāĻ¨ā§āϤāϰ āĻ•āϰ⧁āύ - āĻļ⧁āϰ⧁ āĻ•āϰ⧁āύ + āĻŽāĻžāχāĻ—ā§āϰ⧇āϟ + āĻ¸ā§āϟāĻžāĻ°ā§āϟ āĻ…āĻ§ā§āϝāĻžā§Ÿāϗ⧁āϞāĻŋ āĻĻ⧇āϖ⧁āύ āϏāĻŦ āĻ…āĻ•ā§āώāĻŽ āĻ•āϰ⧁āύ āϏāĻŦ āϏāĻ•ā§āϰāĻŋāϝāĻŧ āĻ•āϰ⧁āύ - āĻŽā§‡āϟāĻžāĻĄāĻžāϟāĻž āϏāϝāĻŧāĻ‚āĻ•ā§āϰāĻŋāϝāĻŧāĻ­āĻžāĻŦ⧇ āĻšāĻžāϞāύāĻžāĻ—āĻžāĻĻ āĻ•āϰ⧁āύ + āĻŽā§‡āϟāĻžāĻĄāĻžāϟāĻž āĻ¸ā§āĻŦāϝāĻŧāĻ‚āĻ•ā§āϰāĻŋāϝāĻŧāĻ­āĻžāĻŦ⧇ āϰāĻŋāĻĢā§āϰ⧇āĻļ āĻŦā§āϝāĻžāĻĒāĻ• āφāĻĒāĻĄā§‡āϟ āĻ…ā§āϝāĻžāĻĒ āĻĒāĻžāĻ˛ā§āϟāĻžāύ⧋āϰ āϏāĻŽāϝāĻŧ āĻ…ā§āϝāĻžāĻĒ⧇āϰ āĻ•āĻ¨ā§āĻŸā§‡āĻ¨ā§āϟ āϞ⧁āĻ•āĻžāύ āĻ“ āĻ¸ā§āĻ•ā§āϰāĻŋāύāϏāϟ āĻŦā§āϞāĻ• āĻ•āϰ⧁āύ āϏ⧇āϟāĻŋāĻ‚āϏ⧇ āϖ⧁āρāϜ⧁āύ @@ -351,7 +351,7 @@ āĻĒ⧜āĻž āĻšāĻšā§āϛ⧇ āĻĒāĻ āύ āϧāϰāύ āϏāĻŦāϏāĻŽā§Ÿ āĻ…āĻ§ā§āϝāĻžā§Ÿ āĻĒāϰāĻŋāĻŦāĻ°ā§āϤāύ āĻĻ⧇āĻ–āĻžāύ - āĻāĻ•āϟāĻžāύāĻž āωāĻ˛ā§āϞāĻŽā§āĻŦ + āĻĢāĻžāρāĻ•āϏāĻš āϞāĻ‚ āĻ¸ā§āĻŸā§āϰāĻŋāĻĒ āĻĒāĻ āύ āϧāϰāύ āĻĻ⧇āĻ–āĻžāύ āϧ⧂āϏāϰ āĻĢāĻŋāϞāϟāĻžāϰāĻĄ āĻ…āĻ§ā§āϝāĻžā§Ÿ āϗ⧁āϞ⧋ āĻā§œāĻŋā§Ÿā§‡ āϝāĻžāύ @@ -360,7 +360,7 @@ ā§§ā§Ž+ āĻ…āύāĻŋāĻˇā§āĻĒāĻ¨ā§āύ āĻšāĻžāϞāύāĻžāĻ—āĻžāĻĻ āϏāĻ‚āĻ—ā§āϰāĻšāĻļāĻžāϞāĻž āĻšāĻžāϞāύāĻžāϗ⧇āĻĻ⧇āϰ āϏāĻŽā§Ÿ āύāϤ⧁āύ āĻŽā§‹ā§œāĻ• āĻāĻŦāĻ‚ āĻŦāĻ°ā§āĻŖāύāĻž āϖ⧁āρāϜ⧁āύ - āĻ—āĻžāρāĻĨ⧁āύāĻŋāĻ•ā§ƒāϤ + āĻĒāĻŋāύ āĻ•āϰāĻž āĻŸā§āĻ°ā§āϝāĻžāĻ•āĻžāϰāϗ⧁āϞāĻŋāϤ⧇ āĻĒā§āϰāĻŦ⧇āĻļ āĻšā§ŸāύāĻŋ: āϰāĻŋāĻĄāĻžāϰ āĻ–ā§‹āϞāĻž āĻĨāĻžāĻ•āϞ⧇ āĻŦāĻ°ā§āϤāĻŽāĻžāύ āĻŽā§‹āĻĄ āϏāĻ‚āĻ•ā§āώ⧇āĻĒ⧇ āĻĻ⧇āĻ–āĻžāύ āωāĻ­āϝāĻŧ @@ -393,11 +393,11 @@ āĻāĻ›āĻžāĻĄāĻŧāĻžāĻ“ āφāĻŽāĻžāϰ āϏāĻ‚āĻ—ā§āϰāĻšāĻļāĻžāϞāĻžāϰ āĻŽāĻžāĻ™ā§āĻ—āĻžāϤ⧇ āĻĒā§āϰāϝāĻŧā§‹āĻ— āĻ•āϰ⧁āύ āĻ…āĻ§ā§āϝāĻžāϝāĻŧ āϏ⧇āϟāĻŋāĻ‚āϏ āφāĻĒāϞ⧋āĻĄā§‡āϰ āϤāĻžāϰāĻŋāĻ– āĻ…āύ⧁āϝāĻžāϝāĻŧā§€ - āϏāĻ‚āĻ—ā§āϰāĻšāĻļāĻžāϞāĻžāϰ āĻŽāĻžāĻ™ā§āĻ—āĻžāϰ āĻŽā§‹ā§œāĻ• āĻšāĻžāϞāύāĻžāĻ—āĻžāĻĻ āĻ•āϰ⧁āύ + āϞāĻžāχāĻŦā§āϰ⧇āϰāĻŋāϰ āĻ•āĻ­āĻžāϰāϗ⧁āϞ⧋ āϰāĻŋāĻĢā§āϰ⧇āĻļ āĻ…āϜāĻžāύāĻž āĻ…āĻŦāĻ¸ā§āĻĨāĻž āĻ…āϜāĻžāύāĻž āϞ⧇āĻ–āĻ• āφāĻĒāύāĻžāϰ āϕ⧋āύ āĻĒāĻŋāύ āĻ•āϰāĻž āĻ‰ā§ŽāϏ āύ⧇āχ - āĻ¸ā§āĻĨāĻžāύ⧀āϝāĻŧ āĻ‰ā§ŽāϏ āύāĻŋāĻ°ā§āĻĻ⧇āĻļāĻŋāĻ•āĻž + āϞ⧋āĻ•āĻžāϞ āϏ⧋āĻ°ā§āϏ āĻ—āĻžāχāĻĄ \"%1$s\" āϏāĻžāĻ°ā§āĻŦāϜāύ⧀āύāĻ­āĻžāĻŦ⧇ āϖ⧁āρāϜ⧁āύ āϏāĻ°ā§āĻŦāĻļ⧇āώ āĻŦā§āϝāĻŦāĻšā§ƒāϤ āĻŸā§āϝāĻžāĻŦ āϗ⧁āϞāĻŋ @@ -406,12 +406,12 @@ āφāĻĒāύāĻŋ āĻāĻ–āύ āĻĒā§āϰāĻ¸ā§āĻĨāĻžāύ āĻ•āϰ⧇āϛ⧇āύ āĻĒā§āϰāĻ¸ā§āĻĨāĻžāύ āĻĒā§āϰāĻ¸ā§āĻĨāĻžāύ āĻ•āϰāĻŦ⧇āύ %1$s āĻĨ⧇āϕ⧇? - āφāĻĒāύāĻžāϰ āϞāĻžāχāĻŦā§āϰ⧇āϰ⧀āϰ āϏāĻŦ āĻŽāĻžāĻ™ā§āĻ—āĻž āĻĢāĻŋāϞāϟāĻžāϰ āĻ•āϰ⧇ + āϞāĻžāχāĻŦā§āϰ⧇āϰāĻŋāϰ āϏāĻŦ āĻāĻ¨ā§āĻŸā§āϰāĻŋ āĻĢāĻŋāĻ˛ā§āϟāĻžāϰ āĻšāĻŦ⧇ āĻĒ⧜āĻžāϰ āχāϤāĻŋāĻšāĻžāϏ⧇ āĻŦāĻŋāϰāϤāĻŋ āĻĻ⧇āϝāĻŧ āĻ›āĻĻā§āĻŽāĻŦ⧇āĻļā§€ āĻŽā§‹āĻĄ - āϕ⧇āĻŦāϞ āĻĄāĻžāωāύāϞ⧋āĻĄ āĻšāϝāĻŧ⧇āϛ⧇ āĻāĻŽāύ - āĻšāĻžāϞāύāĻžāĻ—āĻžāĻĻ āĻ•āϰāĻž āĻšā§Ÿā§‡āϛ⧇ v%1$s āϤ⧇ - āĻšāĻžāϞāύāĻžāĻ—āĻžāĻĻ āĻāϰ āϜāĻ¨ā§āϝ āĻ…āύ⧁āϏāĻ¨ā§āϧāĻžāύ āĻ•āϰ⧁āύ + āĻļ⧁āϧ⧁ āĻĄāĻžāωāύāϞ⧋āĻĄ + v%1$s āĻ āφāĻĒāĻĄā§‡āϟ āĻšā§Ÿā§‡āϛ⧇ + āϏāĻ‚āĻ¸ā§āĻ•āϰāĻŖ āϝāĻžāϚāĻžāχ āĻ“āĻĒ⧇āύ āϏ⧋āĻ°ā§āϏ āϞāĻžāχāϏ⧇āĻ¨ā§āϏ āύāϤ⧁āύ āĻ•āĻŋ āĻ“āϝāĻŧ⧇āĻŦāϏāĻžāχāϟ @@ -459,7 +459,7 @@ āĻ›āĻĻā§āĻŽāĻŦ⧇āĻļā§€ āĻŽā§‹āĻĄ āύāĻŋāĻˇā§āĻ•ā§āϰāĻŋāϝāĻŧ āĻ•āϰ⧁āύ āĻ•āĻŋāϛ⧁ āύāĻŋāĻ°ā§āĻŽāĻžāϤāĻžāĻĻ⧇āϰ āĻ…āϤāĻŋāϰāĻŋāĻ•ā§āϤ āĻ…ā§āϝāĻžāĻĒ āϏ⧀āĻŽāĻžāĻŦāĻĻā§āϧāϤāĻž āϰāϝāĻŧ⧇āϛ⧇ āϝāĻž āĻŦā§āϝāĻžāĻ•āĻ—ā§āϰāĻžāωāĻ¨ā§āĻĄ āĻĒāϰāĻŋāώ⧇āĻŦāĻžāϗ⧁āϞāĻŋāϕ⧇ āĻšāĻ¤ā§āϝāĻž āĻ•āϰ⧇āĨ¤ āĻāχ āĻ“āϝāĻŧ⧇āĻŦāϏāĻžāχāĻŸā§‡ āĻ•āĻŋāĻ­āĻžāĻŦ⧇ āĻāϟāĻŋ āĻ āĻŋāĻ• āĻ•āϰāĻž āϝāĻžāϝāĻŧ āϏ⧇ āϏāĻŽā§āĻĒāĻ°ā§āϕ⧇ āφāϰāĻ“ āϤāĻĨā§āϝ āϰāϝāĻŧ⧇āϛ⧇āĨ¤ āĻĒāϟāĻ­ā§‚āĻŽāĻŋ āĻ•āĻžāĻ°ā§āϝāĻ•āϞāĻžāĻĒ - āĻĄāĻžāĻŽā§āĻĒ āĻ•ā§āĻ°ā§āϝāĻžāĻļ āϞāĻ— + āĻ•ā§āĻ°ā§āϝāĻžāĻļ āϞāĻ— āĻļā§‡ā§ŸāĻžāϰ āĻāχāϚāϟāĻŋāϟāĻŋāĻĒāĻŋāĻāϏ āĻāϰ āωāĻĒāϰ āĻĄāĻŋāĻāύāĻāϏ āĻāĻŽāφāχāχāωāφāχ āĻ…āĻĒā§āϟāĻŋāĻŽāĻžāχāĻœā§‡āĻļāύ āύāĻŋāĻˇā§āĻ•ā§āϰāĻŋāϝāĻŧ āĻĨāĻžāĻ•āϞ⧇ āĻŦā§āϝāĻžāĻ•āφāĻĒ/āϰāĻŋāĻ¸ā§āĻŸā§‹āϰ āϏāĻ āĻŋāĻ•āĻ­āĻžāĻŦ⧇ āĻ•āĻžāϜ āύāĻžāĻ“ āĻ•āϰāϤ⧇ āĻĒāĻžāϰ⧇āĨ¤ āϏāĻŽā§āĻ­āĻŦāϤ āĻ…āύ⧁āĻĒāĻ¸ā§āĻĨāĻŋāϤ āĻāĻ•ā§āϏāĻŸā§‡āύāĻļāύ āχāύāĻ¸ā§āϟāϞ āĻ•āϰāϤ⧇ āĻšāĻŦ⧇ āĻāĻŦāĻ‚ āĻŸā§āĻ°ā§āϝāĻžāĻ•āĻŋāĻ‚ āϏāĻžāĻ°ā§āĻ­āĻŋāϏāϗ⧁āϞ⧋āϤ⧇ āĻĒāϰ⧇ āϞāĻ—āχāύ āĻ•āϰāϤ⧇ āĻšāĻŦ⧇ āĻāϗ⧁āϞ⧋ āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻ•āϰāϤ⧇āĨ¤ @@ -476,7 +476,7 @@ āĻ¸ā§āĻ•ā§āϰāϞ⧇ āĻŽā§‡āύ⧁ āϞ⧁āĻ•āĻžāύ⧋āϰ āϜāĻ¨ā§āϝ āϏāĻ‚āĻŦ⧇āĻĻāύāĻļā§€āϞāϤāĻž āĻ­ā§‚āĻĻ⧃āĻļā§āϝ āĻĒā§āϰāϤāĻŋāĻ•ā§ƒāϤāĻŋ - āĻ˜ā§‚āĻ°ā§āĻŖāύ⧇āϰ āϧāϰāύ + āĻ˜ā§‚āĻ°ā§āĻŖāύ āĻĄāĻžāύ āĻŦāĻžāĻŽ āĻĒāϰāĻŦāĻ°ā§āϤ⧀ @@ -515,7 +515,7 @@ āĻāĻ–āύāχ āĻĄāĻžāωāύāϞ⧋āĻĄ āĻļ⧁āϰ⧁ āĻ•āϰ⧁āύ āϏāĻ•āϞ āĻ¤ā§āϰ⧁āϟāĻŋ āĻĻ⧇āϖ⧁āύ āĻāχ āϏāĻŋāϰāĻŋāĻœā§‡āϰ āϏāĻŦ āĻŦāĻžāϤāĻŋāϞ āĻ•āϰ⧁āύ - āĻ¸ā§āĻĨāĻžāύ⧀āϝāĻŧ āĻ‰ā§ŽāϏ + āϞ⧋āĻ•āĻžāϞ āϏ⧋āĻ°ā§āϏ āφāĻĒāύāĻžāϰ āĻāĻ–āύāĻ“ āϕ⧋āύ āĻŦāĻŋāĻ­āĻžāĻ— āύ⧇āχ | āĻāχ āĻ…ā§āϝāĻžāĻ¨ā§āĻĄā§āϰāϝāĻŧ⧇āĻĄ āϏāĻ‚āĻ¸ā§āĻ•āϰāĻŖāϟāĻŋ āφāϰ āϏāĻŽāĻ°ā§āĻĨāĻŋāϤ āύāϝāĻŧ āĻĒāϰāĻŋāĻŦāĻ°ā§āϤāύ āύāĻŋāĻļā§āϚāĻŋāϤ āĻ•āϰāϤ⧇ āĻĒā§āϰāĻŽāĻžāĻŖā§€āĻ•āϰāĻŖ āĻ•āϰ⧁āύ @@ -554,13 +554,13 @@ %1$d āφāĻĒāĻĄā§‡āϟ(āϗ⧁āϞāĻŋ) āĻŦā§āϝāĻ°ā§āĻĨ āĻšā§Ÿā§‡āϛ⧇ āϏāĻŽā§āĻĒā§‚āĻ°ā§āĻŖ āĻĒā§āϰāĻ•āĻžāĻļāĻŋāϤ āϏāĻ°ā§āĻŦāĻļ⧇āώ āĻšāĻžāϞāύāĻžāĻ—āĻžāĻĻ āĻšā§‡āĻ• - āφāĻĒāύāĻŋ āĻ•āĻŋ \"%s\" āĻŦāĻŋāĻ­āĻžāĻ—āϟāĻŋ āĻŽā§āϛ⧇ āĻĢ⧇āϞāϤ⧇ āϚāĻžāύ? + āφāĻĒāύāĻŋ āĻ•āĻŋ “%s” āĻ•ā§āϝāĻžāϟāĻžāĻ—āϰāĻŋāϟāĻŋ āĻŽā§āϛ⧇ āĻĢ⧇āϞāϤ⧇ āϚāĻžāύ? āϏāĻŽā§āĻĒā§āϰāϤāĻŋ āφāĻĒāύāĻžāϰ āĻšāĻžāϞāύāĻžāĻ—āĻžāĻĻāĻ•ā§ƒāϤ āĻŽāĻžāĻ™ā§āĻ—āĻž āĻĻ⧇āϖ⧁āύ āφāĻĒāύāĻŋ āĻ•āĻŋ āύāĻŋāĻļā§āϚāĻŋāϤ? āĻ…āĻĒāĻ āĻŋāϤ āϏāĻ‚āĻ–ā§āϝāĻž - āϏāĻŦāĻ•āĻŋāϛ⧁ āĻŽā§āϛ⧁āύ + āϏāĻŦāĻ•āĻŋāϛ⧁ āĻŽā§āϛ⧇ āĻĢ⧇āϞ⧁āύ āϞ⧇āĻ–āĻž āĻĻ⧇āĻ–āĻžāύ - āĻŦāĻŋāĻ­āĻžāĻ— āĻŽā§āϛ⧁āύ + āĻ•ā§āϝāĻžāϟāĻžāĻ—āϰāĻŋ āĻĄāĻŋāϞāĻŋāϟ āĻ˛ā§āϝāĻžāϭ⧇āĻŖā§āĻĄāĻžāϰ āϖ⧁āρāϜ⧁āύâ€Ļ āĻļ⧁āϧ⧁ āĻĒā§āϰāĻšā§āĻ›āĻĻāϏāĻš āĻ—ā§āϰāĻŋāĻĄ @@ -583,15 +583,15 @@ āĻŽā§āϝāĻžāύ⧁āϝāĻŧāĻžāϞ āĻ“ āϏāϝāĻŧāĻ‚āĻ•ā§āϰāĻŋāϝāĻŧ āĻŦā§āϝāĻžāĻ•āφāĻĒ āĻ…ā§āϝāĻžāĻĒ āϞāĻ•,āύāĻŋāϰāĻžāĻĒāĻĻ āĻĒāĻ°ā§āĻĻāĻž āĻ…āĻĒāĻ āĻŋāϤ āĻ…āĻ§ā§āϝāĻžāϝāĻŧ āĻĨāĻžāĻ•āĻžāϝāĻŧ āĻāĻĄāĻŧāĻŋāϝāĻŧ⧇ āϝāĻžāĻ“āϝāĻŧāĻž āĻšāϝāĻŧ⧇āϛ⧇ - āĻ¸ā§āĻĨāĻžāĻ¨ā§€ā§Ÿ + āϞ⧋āĻ•āĻžāϞ āĻ•ā§āϞāĻŋāĻĒāĻŦā§‹āĻ°ā§āĻĄā§‡ āĻ•āĻĒāĻŋ āĻ•āϰ⧁āύ āϏāĻŽāĻ¸ā§āϤ āĻĒāĻžāĻ āĻ• āϏ⧇āϟāĻŋāĻ‚āϏ āφāĻŦāĻžāϰ āĻĒāϰāĻŋāϏāĻ‚āĻ–ā§āϝāĻžāύ āĻĄāĻžāωāύāϞ⧋āĻĄ āĻšāϝāĻŧ⧇āϛ⧇ āĻļ⧁āϰ⧁ āĻšāϝāĻŧ⧇āϛ⧇ āĻ…ā§āϝāĻžāĻĒ āϞāĻ• āϚāĻžāϞ⧁ āĻĨāĻžāĻ•āϞ⧇ Widget āĻĒāĻžāĻ“āϝāĻŧāĻž āϝāĻžāϝāĻŧ āύāĻž - āĻŦāĻŋāĻ­āĻžāĻ— āφāĻĒāĻĄā§‡āϟ āĻ•āϰ⧁āύ - āĻāϞ⧋āĻŽā§‡āϞ⧋ āĻāĻ¨ā§āĻŸā§āϰāĻŋ āϖ⧁āϞ⧁āύ + āĻ•ā§āϝāĻžāϟāĻžāĻ—āϰāĻŋ āφāĻĒāĻĄā§‡āϟ + āϝ⧇āϕ⧋āύ⧋ āĻ°ā§āϝāĻžāĻ¨ā§āĻĄāĻŽ āĻāĻ¨ā§āĻŸā§āϰāĻŋ āĻĒāĻĄāĻŧāĻž āϚāĻžāϞāĻŋāϝāĻŧ⧇ āϝāĻžāύ āĻŦā§‹āϤāĻžāĻŽ āĻĄāĻžāωāύāϞ⧋āĻĄ āĻšā§‡āĻ• āĻ•āϰāĻž āĻšāĻšā§āϛ⧇ āĻĒ⧃āĻˇā§āĻ āĻžāϰ āĻĢāĻžāχāϞ āĻĒāĻžāĻĨ āϖ⧁āρāĻœā§‡ āĻĒāĻžāĻ“āϝāĻŧāĻž āϝāĻžāϝāĻŧāύāĻŋ %d @@ -607,7 +607,7 @@ āύāϟ āϏāĻŋāϞ⧇āĻ•ā§āĻŸā§‡āĻĄ āĻ¸ā§āĻ•ā§āϝāĻžāύāϞ⧇āϟāϰ āύ⧇āĻ­āĻŋāϗ⧇āϟ āφāĻĒ - āĻĄāĻžāϟāĻž āĻ…āύ āĻ¸ā§āĻŸā§‹āϰ⧇āϜ + āĻĄāĻžāϟāĻž āĻ…ā§āϝāĻžāĻ¨ā§āĻĄ āĻ¸ā§āĻŸā§‹āϰ⧇āϜ āĻ•āĻžāĻ¸ā§āϟāĻŽāĻžāχāϜāĻĄ āφāύāĻžāϰ āĻŦā§āϝāĻŦāϧāĻžāύ āĻĢā§‹āĻ˛ā§āĻĄāĻžāϰ āύāĻŋāĻ°ā§āĻŦāĻžāϚāύ āĻ•āϰ⧁āύ āĻ…āύāĻŦāĻ°ā§āĻĄāĻŋāĻ‚ āĻ—āĻžāχāĻĄ @@ -626,7 +626,7 @@ āχāĻ¨ā§āϟāĻžāϰāύāĻžāϞ āχāϰāϰ: āĻŦāĻžāĻ•āĻŋ āχāύāĻĢāϰāĻŽā§‡āĻļāύ āĻĒ⧇āϤ⧇ āĻ•ā§āϝāĻžāĻļ āϞāĻ— āĻĻ⧇āϖ⧁āύ āĻ¸ā§āĻ•āĻŋāĻĒ āĻ•āϰāĻž āĻšāϝāĻŧ⧇āϛ⧇ āĻ•āĻžāϰāĻŖ āφāϗ⧇ āϕ⧋āύ āĻšā§āϝāĻžāĻĒā§āϟāĻžāϰ āĻĒāĻĄāĻŧāĻž āĻšāϝāĻŧāύāĻŋ āĻŸā§āϰ⧇āĻ•āĻžāϰ āĻ¸ā§āϕ⧋āϰ - āĻ āĻŋāĻ• āφāϛ⧇ + āĻ“āϕ⧇ āĻĒāϰāĻŦāĻ°ā§āϤ⧀ āφāĻĒāĻĄā§‡āĻŸā§‡āϰ āϏāĻŽā§āĻ­āĻžāĻŦā§āϝ āϏāĻŽāϝāĻŧ āϞāĻŋāĻ‚āĻ• āĻ•āĻĒāĻŋ āĻ•āϰ⧁āύ āĻ¸ā§āĻŦāϝāĻŧāĻ‚āĻ•ā§āϰāĻŋāϝāĻŧ @@ -700,7 +700,7 @@ āϏāϰāĻžāύ āĻāĻ•ā§āϏāĻŸā§‡āύāĻļāύ āϏāϰāĻžāϤ⧇ āϚāĻžāύ? āφāĻĒāύāĻŋ āĻ•āĻŋ āύāĻŋāĻļā§āϚāĻŋāϤ \"%s\" āĻāĻ•ā§āϏāĻŸā§‡āύāĻļāύ āϏāϰāĻžāϤ⧇ āϚāĻžāύ? - āĻ­āĻžāĻ°ā§āϏāύ + āϏāĻ‚āĻ¸ā§āĻ•āϰāĻŖ āĻ­āĻžāώāĻž āĻŦāϝāĻŧāϏ āϏ⧀āĻŽāĻž āĻāĻ•ā§āϏāĻŸā§‡āύāĻļāύ āχāύāĻ¸ā§āϟāϞ āĻ•āϰāϤ⧇ āĻ…āύ⧁āĻŽāϤāĻŋ āϞāĻžāĻ—āĻŦ⧇āĨ¤ āĻ…āύ⧁āĻŽāϤāĻŋ āĻĻāĻŋāϤ⧇ āĻāĻ–āĻžāύ⧇ āĻŸā§āϝāĻžāĻĒ āĻ•āϰ⧁āύāĨ¤ @@ -756,7 +756,7 @@ āύāĻŋāĻˇā§āĻ•ā§āϰāĻŋāϝāĻŧ āϚāĻ“āĻĄāĻŧāĻž āĻ›āĻŦāĻŋ āĻĒā§āϝāĻžāύ āĻšāĻŦ⧇ āĻ…āĻŸā§‹āĻŽā§‡āϟāĻŋāĻ• āϜ⧁āĻŽ āĻšāĻŦ⧇ āϚāĻ“āĻĄāĻŧāĻž āĻ›āĻŦāĻŋāϤ⧇ - āĻĒā§‹āĻ°ā§āĻŸā§āϰ⧇āϟ āωāĻ˛ā§āϟāĻžāύ⧋ āĻšāĻŦ⧇ + āωāĻ˛ā§āĻŸā§‹ āĻĒā§āϰāϤāĻŋāĻ•ā§ƒāϤāĻŋ āϜ⧁āĻŽ āφāωāϟ āĻŦāĻ¨ā§āϧ āĻ¸ā§āĻŸā§‹āϰ⧇āϜ āϞ⧋āϕ⧇āĻļāύ āϏ⧇āϟ āύ⧇āχ āϭ⧁āϞ āϞ⧋āϕ⧇āĻļāύ: %s @@ -826,4 +826,23 @@ āĻāϟāĻŋ %s āĻĨ⧇āϕ⧇ āĻĒā§‚āĻ°ā§āĻŦ⧇ āύāĻŋāĻ°ā§āĻŦāĻžāϚāĻŋāϤ āĻļ⧁āϰ⧁āϰ āϤāĻžāϰāĻŋāĻ– āĻŽā§āϛ⧇ āĻĢ⧇āϞāĻŦ⧇ āĻāϟāĻŋ %s āĻĨ⧇āϕ⧇ āĻĒā§‚āĻ°ā§āĻŦ⧇ āύāĻŋāĻ°ā§āĻŦāĻžāϚāĻŋāϤ āĻļ⧇āώ āϤāĻžāϰāĻŋāĻ– āĻŽā§āϛ⧇ āĻĢ⧇āϞāĻŦ⧇ %s āĻŸā§āĻ°ā§āϝāĻžāĻ•āĻŋāĻ‚ āĻŽā§āϛ⧇ āĻĢ⧇āϞāĻž āĻšāĻŦ⧇? + āϞāĻžāχāĻŦā§āϰ⧇āϰāĻŋ āϏāĻ°ā§āĻŦāĻļ⧇āώ āφāĻĒāĻĄā§‡āϟ: %s + āφāϏāĻ¨ā§āύ āφāĻĒāĻĄā§‡āϟāϗ⧁āϞ⧋ āĻĻ⧇āϖ⧁āύ + āφāϏāĻ¨ā§āύ āĻ—āĻžāχāĻĄ + āφāĻ—āĻžāĻŽā§€ āĻŽāĻžāϏ + āĻ—āϤ āĻŽāĻžāϏ + āĻāϟāĻŋ āϞ⧋āĻ•āĻžāϞ āĻŸā§āĻ°ā§āϝāĻžāĻ•āĻŋāĻ‚ āĻĨ⧇āϕ⧇ āĻŽā§āϛ⧇ āϝāĻžāĻŦ⧇āĨ¤ + āϏāĻ°ā§āĻŦāĻļ⧇āώ: %1$s + āĻ…āϜāĻžāύāĻž + āϞāĻžāχāĻŦā§āϰ⧇āϰāĻŋāϤ⧇ āĻŽāĻŋāϞ āĻĨāĻžāĻ•āĻž āύāĻžāĻŽā§‡ āĻāĻ¨ā§āĻŸā§āϰāĻŋ āϰāϝāĻŧ⧇āϛ⧇āĨ¤\n\nāĻŽāĻžāχāĻ—ā§āϰ⧇āϟ āĻ•āϰāĻžāϰ āϜāĻ¨ā§āϝ āĻāĻ•āϟāĻŋ āĻāĻ¨ā§āĻŸā§āϰāĻŋ āϏāĻŋāϞ⧇āĻ•ā§āϟ āĻ•āϰ⧁āύ āĻ…āĻĨāĻŦāĻž āϤāĻŦ⧁āĻ“ āĻ…ā§āϝāĻžāĻĄ āĻ•āϰ⧁āύāĨ¤ + āĻŽāĻžāχāĻ—ā§āϰ⧇āϟ + āĻŽāĻžāχāĻ—ā§āϰ⧇āϟ āĻ•āϰāĻŦ⧇āύ āύāĻž + āĻāĻ–āύāχ āĻŽāĻžāχāĻ—ā§āϰ⧇āϟ + āĻ•āĻĒāĻŋ + āĻŽāĻžāχāĻ—ā§āϰ⧇āϟ + āĻ•ā§āϝāĻžāύāϏ⧇āϞ + āĻāχ āĻŽāĻžāĻ™ā§āĻ—āĻžāϟāĻŋ āϏāĻŽā§āĻĒā§‚āĻ°ā§āĻŖ āĻšā§Ÿā§‡āϛ⧇, āĻ…āĻĨāĻŦāĻž āĻĒāϰāĻŦāĻ°ā§āϤ⧀ āĻĒā§āϰāĻ•āĻžāĻļ⧇āϰ āϕ⧋āύ⧋ āϏāĻŽā§āĻ­āĻžāĻŦā§āϝ āϤāĻžāϰāĻŋāĻ– āύ⧇āχāĨ¤ + āĻĄāĻŋāĻĢāĻ˛ā§āϟ āχāωāϜāĻžāϰ āĻāĻœā§‡āĻ¨ā§āϟ āĻ¸ā§āĻŸā§āϰāĻŋāĻ‚ āϰāĻŋāϏ⧇āϟ + āĻĄāĻžāωāύāϞ⧋āĻĄ āĻ•āϰāĻž āĻ…āĻ§ā§āϝāĻžā§Ÿāϗ⧁āϞ⧋ āĻœā§‹āϰāĻĒā§‚āĻ°ā§āĻŦāĻ• āĻĒ⧁āύāϰāĻžā§Ÿ āϝāĻžāϚāĻžāχ āĻšāĻŦ⧇ + āĻĄāĻžāωāύāϞ⧋āĻĄ āχāύāĻĄā§‡āĻ•ā§āϏ āĻ…āĻ•āĻžāĻ°ā§āϝāĻ•āϰ āĻšā§Ÿā§‡āϛ⧇ diff --git a/i18n/src/commonMain/moko-resources/ca/plurals.xml b/i18n/src/commonMain/moko-resources/ca/plurals.xml index db6f57a18c..e485426e8e 100644 --- a/i18n/src/commonMain/moko-resources/ca/plurals.xml +++ b/i18n/src/commonMain/moko-resources/ca/plurals.xml @@ -95,4 +95,19 @@ %1$s pàgines %1$s pàgines + + Voleu migrar %1$d element? + Voleu migrar %1$d elements? + Voleu migrar %1$d elements? + + + Voleu copiar %1$d element? + Voleu copiar %1$d elements? + Voleu copiar %1$d elements? + + + S’ha omès un element + S’han omès %1$d elements + S’han omès %1$d elements + diff --git a/i18n/src/commonMain/moko-resources/ca/strings.xml b/i18n/src/commonMain/moko-resources/ca/strings.xml index 1124631a0f..d37917a4de 100644 --- a/i18n/src/commonMain/moko-resources/ca/strings.xml +++ b/i18n/src/commonMain/moko-resources/ca/strings.xml @@ -333,7 +333,7 @@ Inverteix la selecciÃŗ Fixades Fixa - Segueix + Afegeix un seguiment Espaiat lateral Tira vertical amb separaciÃŗ Desfixa @@ -679,8 +679,8 @@ Copia al porta-retalls Actualitza la categoria Divideix les imatges altes - AcciÃŗ en lliscar cap a la dreta - AcciÃŗ en lliscar cap a l’esquerra + En lliscar cap a la dreta en un capítol + En lliscar cap a l’esquerra en un capítol Toca dues vegades per a ampliar SuperposiciÃŗ Inverteix l’orientaciÃŗ de les pàgines amples rotades @@ -833,4 +833,71 @@ Monocrom Llista de la biblioteca Segueix de manera privada + DÃŗna + Capítols no llegits + Notes + Edita les notes + Tanca la pestanya + Gatputxino + Renderitza les imatges a les descripcions dels mangues + Comportament + Marca els capítols duplicats de llegits com a llegits + DesprÊs de llegir un capítol + DesprÊs d’obtenir un capítol nou + Amaga els indicadors de capítols que manquen + No permetis noms de fitxer que no siguin ASCII + Assegura la compatibiloitat amb alguns mitjans d’emmagatzematge que no admeten Unicode. Quan ho activeu, caldrà que canvieu de nom manualment les carpetes de les fonts i dels mangues substituint-ne els caràcters que no siguin ASCII per llurs representacions hexadecimals UTF-8 en minÃēscules. No cal que canvieu de nom els fitxers dels capítols. + Utilitza el decodificador antic per al lector de tira llarga + No s’ha pogut crear el directori de baixada + No s’ha pogut crear el directori: %s + Baixades concurrents de fonts + Baixades concurrents de pàgines + Pàgines baixades simultàniament per font + Suprimireu elements de la base de dades + Els capítols llegits i el progrÊs dels elements que no siguin a la biblioteca es perdran + Conserva els elements amb capítols llegits + Actualitza els títols dels mangues de la biblioteca perquè encaixin amb la font + Advertència: si un manga canvia de nom, se suprimirà de la cua de baixades (si hi Ês). + S’està iniciant la sessiÃŗâ€Ļ + Possibles duplicats + Ja teniu elements a la biblioteca amb un nom similar.\n\nSeleccioneu un element per a migrar-lo o afegiu-lo igualment. + M’ha agradat la part en quèâ€Ļ + Seleccionats + Disponibles + Selecciona-ho tot + No seleccionis res + Selecciona les fonts activades + Selecciona les fonts fixades + Continua + Dades que es migraran + Suprimeix les baixades de l’element actual desprÊs de la migraciÃŗ + Paraules clau addicionals (opcional) + Ajuda a acotar els resultats de la cerca afegint-hi paraules clau addicionals + Amaga els elements sense coincidències + Amaga els elements sense capítols nous + Mostra un element nomÊs si la coincidència tÊ capítols addicionals + Aquestes opcions sÃŗn lentes i perilloses, i poden implicar restriccions per part de les fonts + Mode de cerca avançat + Parteix el títol en paraules clau per a una cerca mÊs àmplia + Cerca coincidències basant-se en el nÃēmero de capítol + Si ho activeu, cerca la coincidència de mÊs endavant. En cas contrari, agafa la primera coincidència per prioritat de les fonts. + MigraciÃŗ + S’està migrant (%1$d/%2$d) + Copia + Migra + No s’ha trobat cap alternativa + Darrer: %1$s + Desconegut + Cerca manualment + No migris + Migra ara + Copia ara + Voleu aturar la migraciÃŗ? + Atura + Cancel¡la + Copia + Migra + Cancel¡la + Cancel¡la + No s’ha trobat cap capítol, aquest element no es pot utilitzar per a migrar diff --git a/i18n/src/commonMain/moko-resources/ceb/plurals.xml b/i18n/src/commonMain/moko-resources/ceb/plurals.xml index 111de37f15..71880a9247 100644 --- a/i18n/src/commonMain/moko-resources/ceb/plurals.xml +++ b/i18n/src/commonMain/moko-resources/ceb/plurals.xml @@ -20,4 +20,8 @@ %1$s ang nahibilin %1$s ang nahibilin + + Ugma + Sa %1$d ka adlaw + diff --git a/i18n/src/commonMain/moko-resources/ceb/strings.xml b/i18n/src/commonMain/moko-resources/ceb/strings.xml index c79f8f0e60..05e96a8894 100644 --- a/i18n/src/commonMain/moko-resources/ceb/strings.xml +++ b/i18n/src/commonMain/moko-resources/ceb/strings.xml @@ -442,4 +442,5 @@ Dili karon Gikinahanlan ang WebView alang sa Mihon *gikinahanlan + Napili diff --git a/i18n/src/commonMain/moko-resources/fil/plurals.xml b/i18n/src/commonMain/moko-resources/fil/plurals.xml index f7ca20c512..5cf2086a3f 100644 --- a/i18n/src/commonMain/moko-resources/fil/plurals.xml +++ b/i18n/src/commonMain/moko-resources/fil/plurals.xml @@ -85,7 +85,7 @@ Kopyahin ang %1$d na mga entry? - Mayroong entry ay nalaktawan - %1$d na mga entry ay nalaktawan + Isang entry ang nilaktawan + %1$d (na) entry ang nilaktawan diff --git a/i18n/src/commonMain/moko-resources/fil/strings.xml b/i18n/src/commonMain/moko-resources/fil/strings.xml index 91c27c05fc..78991eb567 100644 --- a/i18n/src/commonMain/moko-resources/fil/strings.xml +++ b/i18n/src/commonMain/moko-resources/fil/strings.xml @@ -907,4 +907,6 @@ Kasabay na pag-download ng pahina Mga pahina na nai-download nang sabay-sabay kada source Isara ang tab + Catppuccin + Maaaring Magamit diff --git a/i18n/src/commonMain/moko-resources/fr/strings.xml b/i18n/src/commonMain/moko-resources/fr/strings.xml index ce7568179d..664699d89e 100644 --- a/i18n/src/commonMain/moko-resources/fr/strings.xml +++ b/i18n/src/commonMain/moko-resources/fr/strings.xml @@ -903,4 +903,10 @@ Annuler Donation Rendre les images dans les descriptions de mangas + Fermer l\'onglet + Interdire les noms de fichiers non ASCII + Assure la compatibilitÊ avec certains supports de stockage qui ne prennent pas en charge Unicode. Lorsque cette option est activÊe, vous devrez renommer manuellement les dossiers source et manga en remplaçant les caractères non ASCII par leur reprÊsentation hexadÊcimale UTF-8 en minuscules. Les fichiers de chapitre n\'ont pas besoin d\'ÃĒtre renommÊs. + TÊlÊchargements simultanÊs de sources + TÊlÊchargements simultanÊs de pages + Pages tÊlÊchargÊes simultanÊment par source diff --git a/i18n/src/commonMain/moko-resources/it/plurals.xml b/i18n/src/commonMain/moko-resources/it/plurals.xml index df452a6942..2e84ecf0af 100644 --- a/i18n/src/commonMain/moko-resources/it/plurals.xml +++ b/i18n/src/commonMain/moko-resources/it/plurals.xml @@ -100,4 +100,14 @@ Migrare %1$d voci? Migrare %1$d voci? + + Copia %1$d voce? + Copia %1$d voci? + Copia %1$d voci? + + + Una voce è stata saltata + %1$d voci sono state saltate + %1$d voci sono state saltate + diff --git a/i18n/src/commonMain/moko-resources/it/strings.xml b/i18n/src/commonMain/moko-resources/it/strings.xml index 5eab673cd6..8c0e44ee9f 100644 --- a/i18n/src/commonMain/moko-resources/it/strings.xml +++ b/i18n/src/commonMain/moko-resources/it/strings.xml @@ -216,7 +216,7 @@ Fonte locale Eliminare i capitoli scaricati? In pausa - Tracking + Tracciamento Aggiungere alla libreria? Download in pausa Migra @@ -668,7 +668,7 @@ Punteggio medio %d o Totale - Trackers + Tracciatori %d s %d g Non ora @@ -869,4 +869,44 @@ Creazione della cartella download fallita Creazione della cartella %s fallita Mi è piaciuta la parte doveâ€Ļ + Dona + Chiudi scheda + Renderizza immagini nella descrizioni dei manga + Nascondi gli indicatori dei capitoli mancanti + Non consentire nomi di file non ASCII + Garantisce la compatibilità con alcuni supporti di memorizzazione che non supportano Unicode. Quando questa opzione è abilitata, sarà necessario rinominare manualmente le cartelle sorgente e manga sostituendo i caratteri non ASCII con le relative rappresentazioni esadecimali UTF-8 minuscole. Non è necessario rinominare i file dei capitoli. + Download da fonti simultanee + Download da pagine simultanee + Pagine scaricate simultaneamente per fonte + Dati da migrare + Elimina i download della voce corrente dopo la migrazione + Parole chiave aggiuntive (facoltative) + Aiuta a restringere i risultati della ricerca aggiungendo parole chiave aggiuntive + Nascondi le voci senza corrispondenza + Nascondi le voci senza nuovi capitoli + Mostra la voce solo se ha capitoli aggiuntivi + Queste opzioni sono lente e pericolose e possono comportare restrizioni da parte delle fonti + Modalità di ricerca avanzata + Suddivide il titolo in parole chiave per una ricerca piÚ ampia + Corrispondenza in base al numero del capitolo + Se abilitato, sceglie l\'ultima corrispondenza. Altrimenti, sceglie la prima corrispondenza in base alla priorità della fonte. + Migrazione + Migrazione (%1$d/%2$d) + Copia + Migra + Nessuna alternativa trovata + Ultimo: %1$s + Sconosciuto + Cerca manualmente + Non migrare + Migra ora + Copia ora + Interrompere la migrazione? + Interrompi + Annulla + Copia + Migra + Annulla + Annulla + Nessun capitolo trovato, questa voce non puÃ˛ essere utilizzata per la migrazione diff --git a/i18n/src/commonMain/moko-resources/ko/strings.xml b/i18n/src/commonMain/moko-resources/ko/strings.xml index 25f2fed426..bdc7e8556d 100644 --- a/i18n/src/commonMain/moko-resources/ko/strings.xml +++ b/i18n/src/commonMain/moko-resources/ko/strings.xml @@ -306,7 +306,7 @@ MIUI ėĩœė í™”ę°€ ęēŧė ¸ ėžˆė„ ę˛Ŋ뚰 ë°ąė—…/ëŗĩ뛐 기ëŠĨė´ ė •ėƒ ėž‘ë™í•˜ė§€ ė•Šė„ 눘 ėžˆėŠĩ니다. ëŗĩė›ė´ ė´ë¯¸ ė§„í–‰ė¤‘ ėž…ë‹ˆë‹¤ ė•ąė„ ėžŦė‹œėž‘í•œ í›„ė— ė ėšŠëŠë‹ˆë‹¤ - DNS over HTTPS (DoH) + HTTPSëĨŧ í†ĩ한 DNS (DoH) ë°ė´í„° ë°ąė—…ė´ ė´ë¯¸ ė§„í–‰ė¤‘ėž…ë‹ˆë‹¤ ë°ąė—… ëŗĩ뛐 ė‹¤íŒ¨ @@ -872,4 +872,40 @@ ë™ė‹œ ė†ŒėŠ¤ ë‹¤ėš´ëĄœë“œ ë™ė‹œ íŽ˜ė´ė§€ ë‹¤ėš´ëĄœë“œ ė†ŒėŠ¤ëŗ„ ë™ė‹œė— ë‹¤ėš´ëĄœë“œëœ íŽ˜ė´ė§€ 눘 + ė¸ėƒ ęšŠė—ˆë˜ ëļ€ëļ„ė€â€Ļ + ė‚ŦėšŠ 가ëŠĨ + í™œė„ąí™”ëœ ė†ŒėŠ¤ ė„ íƒ + ęŗ ė •ëœ ė†ŒėŠ¤ ė„ íƒ + ęŗ„ė† + ë§ˆė´ęˇ¸ë ˆė´ė…˜í•  ë°ė´í„° + ë§ˆė´ęˇ¸ë ˆė´ė…˜ 후 현ėžŦ 항ëĒŠ ë‹¤ėš´ëĄœë“œ ė‚­ė œ + ėļ”ę°€ í‚¤ė›Œë“œ (ė„ íƒė‚Ŧ항) + ėļ”ę°€ í‚¤ė›Œë“œëĨŧ ėž…ë Ĩ하면 ę˛€ėƒ‰ 결ęŗŧëĨŧ ėĸížˆëŠ” 데 ë„ė›€ė´ 됩니다 + ėŧėš˜í•˜ė§€ ė•ŠëŠ” 항ëĒŠ 눍揰揰 + ėƒˆëĄœėš´ ėą•í„°ę°€ ė—†ëŠ” 항ëĒŠ 눍揰揰 + ë§¤ėš­ëœ 항ëĒŠė— ėƒˆëĄœėš´ ėą•í„°ę°€ ėžˆė„ 때만 í‘œė‹œ + ė´ ė˜ĩė…˜ë“¤ė€ ė†ë„ę°€ 느ëĻŦęŗ  ėœ„í—˜í•  눘 ėžˆėœŧ늰, ė†ŒėŠ¤ëĄœëļ€í„° ė œí•œė„ ë°›ė„ 눘 ėžˆėŠĩ니다 + 溠揉 ę˛€ėƒ‰ ëĒ¨ë“œ + 렜ëĒŠė„ í‚¤ė›Œë“œëĄœ ëļ„ė„í•´ ę˛€ėƒ‰ ë˛”ėœ„ëĨŧ 넓힙니다 + ėą•í„° 번호 揰뤀 ë§¤ėš­ + í™œė„ąí™”ëœ ę˛Ŋ뚰 가ėžĨ ė§„ë„ę°€ 나간 ë§¤ėš­ė„ ė„ íƒí•Šë‹ˆë‹¤. ęˇ¸ë ‡ė§€ ė•Šėœŧ늴 ė†ŒėŠ¤ ėš°ė„ ėˆœėœ„ė— 따ëŧ ė˛Ģ ë˛ˆė§¸ ë§¤ėš­ė„ ė„ íƒí•Šë‹ˆë‹¤. + ë§ˆė´ęˇ¸ë ˆė´ė…˜ + ë§ˆė´ęˇ¸ë ˆė´ė…˜ (%1$d/%2$d) + ëŗĩė‚Ŧ + ë§ˆė´ęˇ¸ë ˆė´ė…˜ + ëŒ€ė•ˆė„ ė°žė„ 눘 ė—†ėŠĩ니다 + ėĩœė‹ : %1$s + ė•Œ 눘 ė—†ėŒ + ėˆ˜ë™ ę˛€ėƒ‰ + ë§ˆė´ęˇ¸ë ˆė´ė…˜í•˜ė§€ ė•Šę¸° + ė§€ę¸ˆ ë§ˆė´ęˇ¸ë ˆė´ė…˜ + ė§€ę¸ˆ ëŗĩė‚Ŧ + ë§ˆė´ęˇ¸ë ˆė´ė…˜ė„ ė¤‘ė§€í•˜ė‹œę˛ ėŠĩ니까? + 뤑맀 + ėˇ¨ė†Œ + ëŗĩė‚Ŧ + ë§ˆė´ęˇ¸ë ˆė´ė…˜ + ėˇ¨ė†Œ + ėˇ¨ė†Œ + ėą•í„°ëĨŧ ė°žė„ 눘 뗆떴 ė´ 항ëĒŠė€ ë§ˆė´ęˇ¸ë ˆė´ė…˜ė— ė‚ŦėšŠí•  눘 ė—†ėŠĩ니다 diff --git a/i18n/src/commonMain/moko-resources/pt-rBR/plurals.xml b/i18n/src/commonMain/moko-resources/pt-rBR/plurals.xml index f83c2407bc..e55abba583 100644 --- a/i18n/src/commonMain/moko-resources/pt-rBR/plurals.xml +++ b/i18n/src/commonMain/moko-resources/pt-rBR/plurals.xml @@ -93,16 +93,21 @@ Migrar %1$d entrada? Migrar %1$d entradas? - + Copiar %1$d entrada? Copiar %1$d entradas? - + Uma entrada foi ignorada %1$d entradas foram ignoradas + + + + 1 pÃĄgina + %1$s pÃĄginas diff --git a/i18n/src/commonMain/moko-resources/pt-rBR/strings.xml b/i18n/src/commonMain/moko-resources/pt-rBR/strings.xml index 57da5b4ac9..9bb51d4df3 100644 --- a/i18n/src/commonMain/moko-resources/pt-rBR/strings.xml +++ b/i18n/src/commonMain/moko-resources/pt-rBR/strings.xml @@ -65,7 +65,7 @@ Monitoramento Avançado Sobre - Tamanho da grade + Itens por linha Retrato Paisagem AtualizaçÃĩes automÃĄticas @@ -687,7 +687,7 @@ Rotacionar pÃĄginas largas para caber Inverter a orientaÃ§ÃŖo das pÃĄginas largas rotacionadas InformaçÃĩes de depuraÃ§ÃŖo - AÃ§ÃŖo de deslizar para a direita + Capitulo ao deslizar para a direita AÃ§ÃŖo de deslizar para a esquerda Toque duplo para dar zoom PrÃŗxima atualizaÃ§ÃŖo esperada @@ -901,4 +901,7 @@ Editar anotaçÃĩes AnotaçÃĩes Catppuccin + Doar + Fechar aba + Renderizar imagens em descriçÃĩes de mangÃĄ diff --git a/i18n/src/commonMain/moko-resources/ru/plurals.xml b/i18n/src/commonMain/moko-resources/ru/plurals.xml index 945456e523..d9ff94cf45 100644 --- a/i18n/src/commonMain/moko-resources/ru/plurals.xml +++ b/i18n/src/commonMain/moko-resources/ru/plurals.xml @@ -61,10 +61,10 @@ %d ҁĐĩŅ€Đ˛Đ¸ŅĐžĐ˛ ĐžŅ‚ŅĐģĐĩĐļиваĐŊĐ¸Ņ - ĐžŅ‚ŅŅƒŅ‚ŅŅ‚Đ˛ŅƒĐĩŅ‚ %d ĐŗĐģава - ĐžŅ‚ŅŅƒŅ‚ŅŅ‚Đ˛ŅƒŅŽŅ‚ %d ĐŗĐģĐ°Đ˛Ņ‹ - ĐžŅ‚ŅŅƒŅ‚ŅŅ‚Đ˛ŅƒŅŽŅ‚ %d ĐŗĐģав - ĐžŅ‚ŅŅƒŅ‚ŅŅ‚Đ˛ŅƒŅŽŅ‚ %d ĐŗĐģав + ĐžŅ‚ŅŅƒŅ‚ŅŅ‚Đ˛ŅƒĐĩŅ‚ %d ĐŗĐģава в Đ¸ŅŅ‚ĐžŅ‡ĐŊиĐēĐĩ иĐģи ĐąŅ‹Đģа ĐžŅ‚Ņ„Đ¸ĐģŅŒŅ‚Ņ€ĐžĐ˛Đ°ĐŊа + ĐžŅ‚ŅŅƒŅ‚ŅŅ‚Đ˛ŅƒŅŽŅ‚ %d ĐŗĐģĐ°Đ˛Ņ‹ в Đ¸ŅŅ‚ĐžŅ‡ĐŊиĐēĐĩ иĐģи ĐąŅ‹Đģи ĐžŅ‚Ņ„Đ¸ĐģŅŒŅ‚Ņ€ĐžĐ˛Đ°ĐŊŅ‹ + ĐžŅ‚ŅŅƒŅ‚ŅŅ‚Đ˛ŅƒŅŽŅ‚ %d ĐŗĐģав в Đ¸ŅŅ‚ĐžŅ‡ĐŊиĐēĐĩ иĐģи ĐąŅ‹Đģи ĐžŅ‚Ņ„Đ¸ĐģŅŒŅ‚Ņ€ĐžĐ˛Đ°ĐŊŅ‹ + ĐžŅ‚ŅŅƒŅ‚ŅŅ‚Đ˛ŅƒŅŽŅ‚ %d ĐŗĐģав в Đ¸ŅŅ‚ĐžŅ‡ĐŊиĐēĐĩ иĐģи ĐąŅ‹Đģи ĐžŅ‚Ņ„Đ¸ĐģŅŒŅ‚Ņ€ĐžĐ˛Đ°ĐŊŅ‹ Đ’Ņ‡ĐĩŅ€Đ° diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/SettingsItems.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/SettingsItems.kt index 0e35f2d334..b03e49a213 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/SettingsItems.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/SettingsItems.kt @@ -23,11 +23,11 @@ import androidx.compose.material.icons.rounded.CheckBoxOutlineBlank import androidx.compose.material.icons.rounded.DisabledByDefault import androidx.compose.material3.Checkbox import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExposedDropdownMenuAnchorType import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.RadioButton import androidx.compose.material3.Surface @@ -296,7 +296,7 @@ fun SelectItem( ) { OutlinedTextField( modifier = Modifier - .menuAnchor(MenuAnchorType.PrimaryNotEditable) + .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable) .fillMaxWidth() .padding( horizontal = SettingsItemsPaddings.Horizontal, diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/FloatingActionButton.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/FloatingActionButton.kt deleted file mode 100644 index c43a6849da..0000000000 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/FloatingActionButton.kt +++ /dev/null @@ -1,131 +0,0 @@ -package tachiyomi.presentation.core.components.material - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.CubicBezierEasing -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.core.tween -import androidx.compose.animation.expandHorizontally -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkHorizontally -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.sizeIn -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.FloatingActionButtonDefaults -import androidx.compose.material3.FloatingActionButtonElevation -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.contentColorFor -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.unit.dp - -/** - * ExtendedFloatingActionButton with custom transition between collapsed/expanded state. - * - * @see androidx.compose.material3.ExtendedFloatingActionButton - */ -@Composable -fun ExtendedFloatingActionButton( - text: @Composable () -> Unit, - icon: @Composable () -> Unit, - onClick: () -> Unit, - modifier: Modifier = Modifier, - expanded: Boolean = true, - interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, - shape: Shape = MaterialTheme.shapes.large, - containerColor: Color = MaterialTheme.colorScheme.primaryContainer, - contentColor: Color = contentColorFor(containerColor), - elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(), -) { - FloatingActionButton( - modifier = modifier, - onClick = onClick, - interactionSource = interactionSource, - shape = shape, - containerColor = containerColor, - contentColor = contentColor, - elevation = elevation, - ) { - val minWidth by animateDpAsState( - targetValue = if (expanded) ExtendedFabMinimumWidth else FabContainerWidth, - animationSpec = tween( - durationMillis = 500, - easing = EasingEmphasizedCubicBezier, - ), - label = "minWidth", - ) - val startPadding by animateDpAsState( - targetValue = if (expanded) ExtendedFabIconSize / 2 else 0.dp, - animationSpec = tween( - durationMillis = if (expanded) 300 else 900, - easing = EasingEmphasizedCubicBezier, - ), - label = "startPadding", - ) - - Row( - modifier = Modifier - .sizeIn(minWidth = minWidth) - .padding(start = startPadding), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center, - ) { - icon() - AnimatedVisibility( - visible = expanded, - enter = ExtendedFabExpandAnimation, - exit = ExtendedFabCollapseAnimation, - ) { - Box(modifier = Modifier.padding(start = ExtendedFabIconPadding, end = ExtendedFabTextPadding)) { - text() - } - } - } - } -} - -private val EasingLinearCubicBezier = CubicBezierEasing(0.0f, 0.0f, 1.0f, 1.0f) -private val EasingEmphasizedCubicBezier = CubicBezierEasing(0.2f, 0.0f, 0.0f, 1.0f) - -private val ExtendedFabMinimumWidth = 80.dp -private val ExtendedFabIconSize = 24.0.dp -private val ExtendedFabIconPadding = 12.dp -private val ExtendedFabTextPadding = 20.dp - -private val ExtendedFabCollapseAnimation = fadeOut( - animationSpec = tween( - durationMillis = 100, - easing = EasingLinearCubicBezier, - ), -) + shrinkHorizontally( - animationSpec = tween( - durationMillis = 500, - easing = EasingEmphasizedCubicBezier, - ), - shrinkTowards = Alignment.Start, -) - -private val ExtendedFabExpandAnimation = fadeIn( - animationSpec = tween( - durationMillis = 200, - delayMillis = 100, - easing = EasingLinearCubicBezier, - ), -) + expandHorizontally( - animationSpec = tween( - durationMillis = 500, - easing = EasingEmphasizedCubicBezier, - ), - expandFrom = Alignment.Start, -) - -private val FabContainerWidth = 56.0.dp diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/util/LazyListState.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/util/LazyListState.kt index 0222d54928..c2dc3f3ac4 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/util/LazyListState.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/util/LazyListState.kt @@ -2,32 +2,9 @@ package tachiyomi.presentation.core.util import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.grid.LazyGridState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.remember -@Composable -fun LazyListState.shouldExpandFAB(): Boolean { - return remember { - derivedStateOf { - (firstVisibleItemIndex == 0 && firstVisibleItemScrollOffset == 0) || - lastScrolledBackward || - !canScrollForward - } - } - .value -} +fun LazyListState.shouldExpandFAB(): Boolean = lastScrolledBackward || !canScrollForward || !canScrollBackward // AY --> -@Composable -fun LazyGridState.shouldExpandFAB(): Boolean { - return remember { - derivedStateOf { - (firstVisibleItemIndex == 0 && firstVisibleItemScrollOffset == 0) || - lastScrolledBackward || - !canScrollForward - } - } - .value -} +fun LazyGridState.shouldExpandFAB(): Boolean = lastScrolledBackward || !canScrollForward || !canScrollBackward // <-- AY