mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-30 22:07:57 +01:00 
			
		
		
		
	Merge branch 'master' into sync-part-final
This commit is contained in:
		| @@ -22,7 +22,7 @@ android { | ||||
|     defaultConfig { | ||||
|         applicationId = "eu.kanade.tachiyomi" | ||||
|  | ||||
|         versionCode = 111 | ||||
|         versionCode = 112 | ||||
|         versionName = "0.14.7" | ||||
|  | ||||
|         buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") | ||||
|   | ||||
| @@ -24,6 +24,8 @@ class BasePreferences( | ||||
|  | ||||
|     fun acraEnabled() = preferenceStore.getBoolean("acra.enable", isPreviewBuildType || isReleaseBuildType) | ||||
|  | ||||
|     fun shownOnboardingFlow() = preferenceStore.getBoolean(Preference.appStateKey("onboarding_complete"), false) | ||||
|  | ||||
|     enum class ExtensionInstaller(val titleRes: StringResource) { | ||||
|         LEGACY(MR.strings.ext_installer_legacy), | ||||
|         PACKAGEINSTALLER(MR.strings.ext_installer_packageinstaller), | ||||
|   | ||||
| @@ -22,8 +22,8 @@ import tachiyomi.domain.chapter.service.ChapterRecognition | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.source.local.isLocal | ||||
| import java.lang.Long.max | ||||
| import java.time.Instant | ||||
| import java.time.ZonedDateTime | ||||
| import java.util.Date | ||||
| import java.util.TreeSet | ||||
|  | ||||
| class SyncChaptersWithSource( | ||||
| @@ -83,7 +83,7 @@ class SyncChaptersWithSource( | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         val rightNow = Date().time | ||||
|         val rightNow = Instant.now().toEpochMilli() | ||||
|  | ||||
|         // Used to not set upload date of older chapters | ||||
|         // to a higher value than newer chapters | ||||
|   | ||||
| @@ -10,8 +10,8 @@ import tachiyomi.domain.manga.repository.MangaRepository | ||||
| import tachiyomi.source.local.isLocal | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.time.Instant | ||||
| import java.time.ZonedDateTime | ||||
| import java.util.Date | ||||
|  | ||||
| class UpdateManga( | ||||
|     private val mangaRepository: MangaRepository, | ||||
| @@ -46,14 +46,14 @@ class UpdateManga( | ||||
|                 // Never refresh covers if the url is empty to avoid "losing" existing covers | ||||
|                 remoteManga.thumbnail_url.isNullOrEmpty() -> null | ||||
|                 !manualFetch && localManga.thumbnailUrl == remoteManga.thumbnail_url -> null | ||||
|                 localManga.isLocal() -> Date().time | ||||
|                 localManga.isLocal() -> Instant.now().toEpochMilli() | ||||
|                 localManga.hasCustomCover(coverCache) -> { | ||||
|                     coverCache.deleteFromCache(localManga, false) | ||||
|                     null | ||||
|                 } | ||||
|                 else -> { | ||||
|                     coverCache.deleteFromCache(localManga, false) | ||||
|                     Date().time | ||||
|                     Instant.now().toEpochMilli() | ||||
|                 } | ||||
|             } | ||||
|  | ||||
| @@ -87,16 +87,16 @@ class UpdateManga( | ||||
|     } | ||||
|  | ||||
|     suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean { | ||||
|         return mangaRepository.update(MangaUpdate(id = mangaId, lastUpdate = Date().time)) | ||||
|         return mangaRepository.update(MangaUpdate(id = mangaId, lastUpdate = Instant.now().toEpochMilli())) | ||||
|     } | ||||
|  | ||||
|     suspend fun awaitUpdateCoverLastModified(mangaId: Long): Boolean { | ||||
|         return mangaRepository.update(MangaUpdate(id = mangaId, coverLastModified = Date().time)) | ||||
|         return mangaRepository.update(MangaUpdate(id = mangaId, coverLastModified = Instant.now().toEpochMilli())) | ||||
|     } | ||||
|  | ||||
|     suspend fun awaitUpdateFavorite(mangaId: Long, favorite: Boolean): Boolean { | ||||
|         val dateAdded = when (favorite) { | ||||
|             true -> Date().time | ||||
|             true -> Instant.now().toEpochMilli() | ||||
|             false -> 0 | ||||
|         } | ||||
|         return mangaRepository.update( | ||||
|   | ||||
| @@ -17,8 +17,7 @@ import tachiyomi.core.util.system.logcat | ||||
| import tachiyomi.domain.track.interactor.GetTracks | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import kotlin.time.Duration.Companion.minutes | ||||
| import kotlin.time.toJavaDuration | ||||
| import java.util.concurrent.TimeUnit | ||||
|  | ||||
| class DelayedTrackingUpdateJob(private val context: Context, workerParams: WorkerParameters) : | ||||
|     CoroutineWorker(context, workerParams) { | ||||
| @@ -63,7 +62,7 @@ class DelayedTrackingUpdateJob(private val context: Context, workerParams: Worke | ||||
|  | ||||
|             val request = OneTimeWorkRequestBuilder<DelayedTrackingUpdateJob>() | ||||
|                 .setConstraints(constraints) | ||||
|                 .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5.minutes.toJavaDuration()) | ||||
|                 .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5, TimeUnit.MINUTES) | ||||
|                 .addTag(TAG) | ||||
|                 .build() | ||||
|  | ||||
|   | ||||
| @@ -14,6 +14,11 @@ import androidx.compose.foundation.layout.size | ||||
| import androidx.compose.foundation.lazy.items | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.outlined.Close | ||||
| import androidx.compose.material.icons.outlined.GetApp | ||||
| import androidx.compose.material.icons.outlined.Public | ||||
| import androidx.compose.material.icons.outlined.Refresh | ||||
| import androidx.compose.material.icons.outlined.Settings | ||||
| import androidx.compose.material.icons.outlined.VerifiedUser | ||||
| import androidx.compose.material3.AlertDialog | ||||
| import androidx.compose.material3.Button | ||||
| import androidx.compose.material3.CircularProgressIndicator | ||||
| @@ -62,6 +67,7 @@ fun ExtensionScreen( | ||||
|     searchQuery: String?, | ||||
|     onLongClickItem: (Extension) -> Unit, | ||||
|     onClickItemCancel: (Extension) -> Unit, | ||||
|     onClickItemWebView: (Extension.Available) -> Unit, | ||||
|     onInstallExtension: (Extension.Available) -> Unit, | ||||
|     onUninstallExtension: (Extension) -> Unit, | ||||
|     onUpdateExtension: (Extension.Installed) -> Unit, | ||||
| @@ -94,6 +100,7 @@ fun ExtensionScreen( | ||||
|                     contentPadding = contentPadding, | ||||
|                     onLongClickItem = onLongClickItem, | ||||
|                     onClickItemCancel = onClickItemCancel, | ||||
|                     onClickItemWebView = onClickItemWebView, | ||||
|                     onInstallExtension = onInstallExtension, | ||||
|                     onUninstallExtension = onUninstallExtension, | ||||
|                     onUpdateExtension = onUpdateExtension, | ||||
| @@ -111,6 +118,7 @@ private fun ExtensionContent( | ||||
|     state: ExtensionsScreenModel.State, | ||||
|     contentPadding: PaddingValues, | ||||
|     onLongClickItem: (Extension) -> Unit, | ||||
|     onClickItemWebView: (Extension.Available) -> Unit, | ||||
|     onClickItemCancel: (Extension) -> Unit, | ||||
|     onInstallExtension: (Extension.Available) -> Unit, | ||||
|     onUninstallExtension: (Extension) -> Unit, | ||||
| @@ -177,6 +185,7 @@ private fun ExtensionContent( | ||||
|                         } | ||||
|                     }, | ||||
|                     onLongClickItem = onLongClickItem, | ||||
|                     onClickItemWebView = onClickItemWebView, | ||||
|                     onClickItemCancel = onClickItemCancel, | ||||
|                     onClickItemAction = { | ||||
|                         when (it) { | ||||
| @@ -217,6 +226,7 @@ private fun ExtensionItem( | ||||
|     item: ExtensionUiModel.Item, | ||||
|     onClickItem: (Extension) -> Unit, | ||||
|     onLongClickItem: (Extension) -> Unit, | ||||
|     onClickItemWebView: (Extension.Available) -> Unit, | ||||
|     onClickItemCancel: (Extension) -> Unit, | ||||
|     onClickItemAction: (Extension) -> Unit, | ||||
|     modifier: Modifier = Modifier, | ||||
| @@ -260,6 +270,7 @@ private fun ExtensionItem( | ||||
|             ExtensionItemActions( | ||||
|                 extension = extension, | ||||
|                 installStep = installStep, | ||||
|                 onClickItemWebView = onClickItemWebView, | ||||
|                 onClickItemCancel = onClickItemCancel, | ||||
|                 onClickItemAction = onClickItemAction, | ||||
|             ) | ||||
| @@ -343,42 +354,80 @@ private fun ExtensionItemActions( | ||||
|     extension: Extension, | ||||
|     installStep: InstallStep, | ||||
|     modifier: Modifier = Modifier, | ||||
|     onClickItemWebView: (Extension.Available) -> Unit = {}, | ||||
|     onClickItemCancel: (Extension) -> Unit = {}, | ||||
|     onClickItemAction: (Extension) -> Unit = {}, | ||||
| ) { | ||||
|     val isIdle = installStep.isCompleted() | ||||
|     Row(modifier = modifier) { | ||||
|         if (isIdle) { | ||||
|             TextButton( | ||||
|                 onClick = { onClickItemAction(extension) }, | ||||
|             ) { | ||||
|                 Text( | ||||
|                     text = when (installStep) { | ||||
|                         InstallStep.Installed -> stringResource(MR.strings.ext_installed) | ||||
|                         InstallStep.Error -> stringResource(MR.strings.action_retry) | ||||
|                         InstallStep.Idle -> { | ||||
|                             when (extension) { | ||||
|                                 is Extension.Installed -> { | ||||
|                                     if (extension.hasUpdate) { | ||||
|                                         stringResource(MR.strings.ext_update) | ||||
|                                     } else { | ||||
|                                         stringResource(MR.strings.action_settings) | ||||
|                                     } | ||||
|                                 } | ||||
|                                 is Extension.Untrusted -> stringResource(MR.strings.ext_trust) | ||||
|                                 is Extension.Available -> stringResource(MR.strings.ext_install) | ||||
|  | ||||
|     Row( | ||||
|         modifier = modifier, | ||||
|         horizontalArrangement = Arrangement.spacedBy(8.dp), | ||||
|     ) { | ||||
|         when { | ||||
|             !isIdle -> { | ||||
|                 IconButton(onClick = { onClickItemCancel(extension) }) { | ||||
|                     Icon( | ||||
|                         imageVector = Icons.Outlined.Close, | ||||
|                         contentDescription = stringResource(MR.strings.action_cancel), | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|             installStep == InstallStep.Error -> { | ||||
|                 IconButton(onClick = { onClickItemAction(extension) }) { | ||||
|                     Icon( | ||||
|                         imageVector = Icons.Outlined.Refresh, | ||||
|                         contentDescription = stringResource(MR.strings.action_retry), | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|             installStep == InstallStep.Idle -> { | ||||
|                 when (extension) { | ||||
|                     is Extension.Installed -> { | ||||
|                         if (extension.hasUpdate) { | ||||
|                             IconButton(onClick = { onClickItemAction(extension) }) { | ||||
|                                 Icon( | ||||
|                                     imageVector = Icons.Outlined.GetApp, | ||||
|                                     contentDescription = stringResource(MR.strings.ext_update), | ||||
|                                 ) | ||||
|                             } | ||||
|                         } | ||||
|                         else -> error("Must not show install process text") | ||||
|                     }, | ||||
|                 ) | ||||
|             } | ||||
|         } else { | ||||
|             IconButton(onClick = { onClickItemCancel(extension) }) { | ||||
|                 Icon( | ||||
|                     imageVector = Icons.Outlined.Close, | ||||
|                     contentDescription = stringResource(MR.strings.action_cancel), | ||||
|                 ) | ||||
|  | ||||
|                         IconButton(onClick = { onClickItemAction(extension) }) { | ||||
|                             Icon( | ||||
|                                 imageVector = Icons.Outlined.Settings, | ||||
|                                 contentDescription = stringResource(MR.strings.action_settings), | ||||
|                             ) | ||||
|                         } | ||||
|                     } | ||||
|                     is Extension.Untrusted -> { | ||||
|                         IconButton(onClick = { onClickItemAction(extension) }) { | ||||
|                             Icon( | ||||
|                                 imageVector = Icons.Outlined.VerifiedUser, | ||||
|                                 contentDescription = stringResource(MR.strings.ext_trust), | ||||
|                             ) | ||||
|                         } | ||||
|                     } | ||||
|                     is Extension.Available -> { | ||||
|                         if (extension.sources.isNotEmpty()) { | ||||
|                             IconButton( | ||||
|                                 onClick = { onClickItemWebView(extension) }, | ||||
|                             ) { | ||||
|                                 Icon( | ||||
|                                     imageVector = Icons.Outlined.Public, | ||||
|                                     contentDescription = stringResource(MR.strings.action_open_in_web_view), | ||||
|                                 ) | ||||
|                             } | ||||
|                         } | ||||
|  | ||||
|                         IconButton(onClick = { onClickItemAction(extension) }) { | ||||
|                             Icon( | ||||
|                                 imageVector = Icons.Outlined.GetApp, | ||||
|                                 contentDescription = stringResource(MR.strings.ext_install), | ||||
|                             ) | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -2,7 +2,7 @@ package eu.kanade.presentation.crash | ||||
|  | ||||
| import androidx.compose.foundation.background | ||||
| import androidx.compose.foundation.layout.Box | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.fillMaxSize | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.outlined.BugReport | ||||
| @@ -47,7 +47,7 @@ fun CrashScreen( | ||||
|             modifier = Modifier | ||||
|                 .padding(vertical = MaterialTheme.padding.small) | ||||
|                 .clip(MaterialTheme.shapes.small) | ||||
|                 .fillMaxWidth() | ||||
|                 .fillMaxSize() | ||||
|                 .background(MaterialTheme.colorScheme.surfaceVariant), | ||||
|         ) { | ||||
|             Text( | ||||
|   | ||||
| @@ -96,7 +96,7 @@ fun MangaScreen( | ||||
|     onAddToLibraryClicked: () -> Unit, | ||||
|     onWebViewClicked: (() -> Unit)?, | ||||
|     onWebViewLongClicked: (() -> Unit)?, | ||||
|     onTrackingClicked: (() -> Unit)?, | ||||
|     onTrackingClicked: () -> Unit, | ||||
|  | ||||
|     // For tags menu | ||||
|     onTagSearch: (String) -> Unit, | ||||
| @@ -229,7 +229,7 @@ private fun MangaScreenSmallImpl( | ||||
|     onAddToLibraryClicked: () -> Unit, | ||||
|     onWebViewClicked: (() -> Unit)?, | ||||
|     onWebViewLongClicked: (() -> Unit)?, | ||||
|     onTrackingClicked: (() -> Unit)?, | ||||
|     onTrackingClicked: () -> Unit, | ||||
|  | ||||
|     // For tags menu | ||||
|     onTagSearch: (String) -> Unit, | ||||
| @@ -481,7 +481,7 @@ fun MangaScreenLargeImpl( | ||||
|     onAddToLibraryClicked: () -> Unit, | ||||
|     onWebViewClicked: (() -> Unit)?, | ||||
|     onWebViewLongClicked: (() -> Unit)?, | ||||
|     onTrackingClicked: (() -> Unit)?, | ||||
|     onTrackingClicked: () -> Unit, | ||||
|  | ||||
|     // For tags menu | ||||
|     onTagSearch: (String) -> Unit, | ||||
|   | ||||
| @@ -92,7 +92,6 @@ private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTIL | ||||
|  | ||||
| @Composable | ||||
| fun MangaInfoBox( | ||||
|     modifier: Modifier = Modifier, | ||||
|     isTabletUi: Boolean, | ||||
|     appBarPadding: Dp, | ||||
|     title: String, | ||||
| @@ -104,6 +103,7 @@ fun MangaInfoBox( | ||||
|     status: Long, | ||||
|     onCoverClick: () -> Unit, | ||||
|     doSearch: (query: String, global: Boolean) -> Unit, | ||||
|     modifier: Modifier = Modifier, | ||||
| ) { | ||||
|     Box(modifier = modifier) { | ||||
|         // Backdrop | ||||
| @@ -162,7 +162,6 @@ fun MangaInfoBox( | ||||
|  | ||||
| @Composable | ||||
| fun MangaActionRow( | ||||
|     modifier: Modifier = Modifier, | ||||
|     favorite: Boolean, | ||||
|     trackingCount: Int, | ||||
|     fetchInterval: Int?, | ||||
| @@ -170,9 +169,10 @@ fun MangaActionRow( | ||||
|     onAddToLibraryClicked: () -> Unit, | ||||
|     onWebViewClicked: (() -> Unit)?, | ||||
|     onWebViewLongClicked: (() -> Unit)?, | ||||
|     onTrackingClicked: (() -> Unit)?, | ||||
|     onTrackingClicked: () -> Unit, | ||||
|     onEditIntervalClicked: (() -> Unit)?, | ||||
|     onEditCategory: (() -> Unit)?, | ||||
|     modifier: Modifier = Modifier, | ||||
| ) { | ||||
|     val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f) | ||||
|  | ||||
| @@ -200,18 +200,16 @@ fun MangaActionRow( | ||||
|                 onClick = onEditIntervalClicked, | ||||
|             ) | ||||
|         } | ||||
|         if (onTrackingClicked != null) { | ||||
|             MangaActionButton( | ||||
|                 title = if (trackingCount == 0) { | ||||
|                     stringResource(MR.strings.manga_tracking_tab) | ||||
|                 } else { | ||||
|                     pluralStringResource(MR.plurals.num_trackers, count = trackingCount, trackingCount) | ||||
|                 }, | ||||
|                 icon = if (trackingCount == 0) Icons.Outlined.Sync else Icons.Outlined.Done, | ||||
|                 color = if (trackingCount == 0) defaultActionButtonColor else MaterialTheme.colorScheme.primary, | ||||
|                 onClick = onTrackingClicked, | ||||
|             ) | ||||
|         } | ||||
|         MangaActionButton( | ||||
|             title = if (trackingCount == 0) { | ||||
|                 stringResource(MR.strings.manga_tracking_tab) | ||||
|             } else { | ||||
|                 pluralStringResource(MR.plurals.num_trackers, count = trackingCount, trackingCount) | ||||
|             }, | ||||
|             icon = if (trackingCount == 0) Icons.Outlined.Sync else Icons.Outlined.Done, | ||||
|             color = if (trackingCount == 0) defaultActionButtonColor else MaterialTheme.colorScheme.primary, | ||||
|             onClick = onTrackingClicked, | ||||
|         ) | ||||
|         if (onWebViewClicked != null) { | ||||
|             MangaActionButton( | ||||
|                 title = stringResource(MR.strings.action_web_view), | ||||
| @@ -226,12 +224,12 @@ fun MangaActionRow( | ||||
|  | ||||
| @Composable | ||||
| fun ExpandableMangaDescription( | ||||
|     modifier: Modifier = Modifier, | ||||
|     defaultExpandState: Boolean, | ||||
|     description: String?, | ||||
|     tagsProvider: () -> List<String>?, | ||||
|     onTagSearch: (String) -> Unit, | ||||
|     onCopyTagToClipboard: (tag: String) -> Unit, | ||||
|     modifier: Modifier = Modifier, | ||||
| ) { | ||||
|     Column(modifier = modifier) { | ||||
|         val (expanded, onExpanded) = rememberSaveable { | ||||
| @@ -406,13 +404,13 @@ private fun MangaAndSourceTitlesSmall( | ||||
| @Composable | ||||
| private fun MangaContentInfo( | ||||
|     title: String, | ||||
|     textAlign: TextAlign? = LocalTextStyle.current.textAlign, | ||||
|     doSearch: (query: String, global: Boolean) -> Unit, | ||||
|     author: String?, | ||||
|     artist: String?, | ||||
|     status: Long, | ||||
|     sourceName: String, | ||||
|     isStubSource: Boolean, | ||||
|     textAlign: TextAlign? = LocalTextStyle.current.textAlign, | ||||
| ) { | ||||
|     val context = LocalContext.current | ||||
|     Text( | ||||
| @@ -556,7 +554,10 @@ private fun MangaSummary( | ||||
|     expanded: Boolean, | ||||
|     modifier: Modifier = Modifier, | ||||
| ) { | ||||
|     val animProgress by animateFloatAsState(if (expanded) 1f else 0f) | ||||
|     val animProgress by animateFloatAsState( | ||||
|         targetValue = if (expanded) 1f else 0f, | ||||
|         label = "summary", | ||||
|     ) | ||||
|     Layout( | ||||
|         modifier = modifier.clipToBounds(), | ||||
|         contents = listOf( | ||||
|   | ||||
| @@ -0,0 +1,62 @@ | ||||
| package eu.kanade.presentation.more.onboarding | ||||
|  | ||||
| import androidx.compose.foundation.layout.Arrangement | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.material3.Button | ||||
| import androidx.compose.material3.HorizontalDivider | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.platform.LocalUriHandler | ||||
| import androidx.compose.ui.tooling.preview.PreviewLightDark | ||||
| import androidx.compose.ui.unit.dp | ||||
| import eu.kanade.presentation.theme.TachiyomiTheme | ||||
| import tachiyomi.i18n.MR | ||||
| import tachiyomi.presentation.core.i18n.stringResource | ||||
|  | ||||
| @Composable | ||||
| internal fun GuidesStep( | ||||
|     onRestoreBackup: () -> Unit, | ||||
| ) { | ||||
|     val handler = LocalUriHandler.current | ||||
|  | ||||
|     Column( | ||||
|         modifier = Modifier.padding(16.dp), | ||||
|         verticalArrangement = Arrangement.spacedBy(8.dp), | ||||
|     ) { | ||||
|         Text(stringResource(MR.strings.onboarding_guides_new_user, stringResource(MR.strings.app_name))) | ||||
|         Button( | ||||
|             modifier = Modifier.fillMaxWidth(), | ||||
|             onClick = { handler.openUri(GETTING_STARTED_URL) }, | ||||
|         ) { | ||||
|             Text(stringResource(MR.strings.getting_started_guide)) | ||||
|         } | ||||
|  | ||||
|         HorizontalDivider( | ||||
|             color = MaterialTheme.colorScheme.onPrimaryContainer, | ||||
|         ) | ||||
|  | ||||
|         Text(stringResource(MR.strings.onboarding_guides_returning_user, stringResource(MR.strings.app_name))) | ||||
|         Button( | ||||
|             modifier = Modifier.fillMaxWidth(), | ||||
|             onClick = onRestoreBackup, | ||||
|         ) { | ||||
|             Text(stringResource(MR.strings.pref_restore_backup)) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| const val GETTING_STARTED_URL = "https://tachiyomi.org/docs/guides/getting-started" | ||||
|  | ||||
| @PreviewLightDark | ||||
| @Composable | ||||
| private fun GuidesStepPreview() { | ||||
|     TachiyomiTheme { | ||||
|         GuidesStep( | ||||
|             onRestoreBackup = {}, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,98 @@ | ||||
| package eu.kanade.presentation.more.onboarding | ||||
|  | ||||
| import androidx.activity.compose.BackHandler | ||||
| import androidx.compose.animation.AnimatedContent | ||||
| import androidx.compose.foundation.background | ||||
| import androidx.compose.foundation.layout.Box | ||||
| import androidx.compose.foundation.layout.fillMaxSize | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.outlined.RocketLaunch | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.mutableIntStateOf | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.setValue | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.draw.clip | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import eu.kanade.domain.ui.UiPreferences | ||||
| import eu.kanade.tachiyomi.util.system.toast | ||||
| import soup.compose.material.motion.animation.materialSharedAxisX | ||||
| import soup.compose.material.motion.animation.rememberSlideDistance | ||||
| import tachiyomi.domain.storage.service.StoragePreferences | ||||
| import tachiyomi.i18n.MR | ||||
| import tachiyomi.presentation.core.components.material.padding | ||||
| import tachiyomi.presentation.core.i18n.stringResource | ||||
| import tachiyomi.presentation.core.screens.InfoScreen | ||||
|  | ||||
| @Composable | ||||
| fun OnboardingScreen( | ||||
|     storagePreferences: StoragePreferences, | ||||
|     uiPreferences: UiPreferences, | ||||
|     onComplete: () -> Unit, | ||||
|     onRestoreBackup: () -> Unit, | ||||
| ) { | ||||
|     val context = LocalContext.current | ||||
|     val slideDistance = rememberSlideDistance() | ||||
|  | ||||
|     var currentStep by remember { mutableIntStateOf(0) } | ||||
|     val steps: List<@Composable () -> Unit> = remember { | ||||
|         listOf( | ||||
|             { ThemeStep(uiPreferences = uiPreferences) }, | ||||
|             { StorageStep(storagePref = storagePreferences.baseStorageDirectory()) }, | ||||
|             // TODO: prompt for notification permissions when bumping target to Android 13 | ||||
|             { GuidesStep(onRestoreBackup = onRestoreBackup) }, | ||||
|         ) | ||||
|     } | ||||
|     val isLastStep = currentStep == steps.size - 1 | ||||
|  | ||||
|     BackHandler(enabled = currentStep != 0, onBack = { currentStep-- }) | ||||
|  | ||||
|     InfoScreen( | ||||
|         icon = Icons.Outlined.RocketLaunch, | ||||
|         headingText = stringResource(MR.strings.onboarding_heading), | ||||
|         subtitleText = stringResource(MR.strings.onboarding_description), | ||||
|         acceptText = stringResource( | ||||
|             if (isLastStep) { | ||||
|                 MR.strings.onboarding_action_finish | ||||
|             } else { | ||||
|                 MR.strings.onboarding_action_next | ||||
|             }, | ||||
|         ), | ||||
|         onAcceptClick = { | ||||
|             if (isLastStep) { | ||||
|                 onComplete() | ||||
|             } else { | ||||
|                 // TODO: this is kind of janky | ||||
|                 if (currentStep == 1 && !storagePreferences.baseStorageDirectory().isSet()) { | ||||
|                     context.toast(MR.strings.onboarding_storage_selection_required) | ||||
|                 } else { | ||||
|                     currentStep++ | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|     ) { | ||||
|         Box( | ||||
|             modifier = Modifier | ||||
|                 .padding(vertical = MaterialTheme.padding.small) | ||||
|                 .clip(MaterialTheme.shapes.small) | ||||
|                 .fillMaxSize() | ||||
|                 .background(MaterialTheme.colorScheme.surfaceVariant), | ||||
|         ) { | ||||
|             AnimatedContent( | ||||
|                 targetState = currentStep, | ||||
|                 transitionSpec = { | ||||
|                     materialSharedAxisX( | ||||
|                         forward = targetState > initialState, | ||||
|                         slideDistance = slideDistance, | ||||
|                     ) | ||||
|                 }, | ||||
|                 label = "stepContent", | ||||
|             ) { | ||||
|                 steps[it]() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,52 @@ | ||||
| package eu.kanade.presentation.more.onboarding | ||||
|  | ||||
| import android.content.ActivityNotFoundException | ||||
| import androidx.compose.foundation.layout.Arrangement | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import androidx.compose.ui.unit.dp | ||||
| import eu.kanade.presentation.more.settings.screen.SettingsDataScreen | ||||
| import eu.kanade.tachiyomi.util.system.toast | ||||
| import tachiyomi.core.preference.Preference | ||||
| import tachiyomi.i18n.MR | ||||
| import tachiyomi.presentation.core.components.material.Button | ||||
| import tachiyomi.presentation.core.i18n.stringResource | ||||
|  | ||||
| @Composable | ||||
| internal fun StorageStep( | ||||
|     storagePref: Preference<String>, | ||||
| ) { | ||||
|     val context = LocalContext.current | ||||
|     val pickStorageLocation = SettingsDataScreen.storageLocationPicker(storagePref) | ||||
|  | ||||
|     Column( | ||||
|         modifier = Modifier.padding(16.dp), | ||||
|         verticalArrangement = Arrangement.spacedBy(8.dp), | ||||
|     ) { | ||||
|         Text( | ||||
|             stringResource( | ||||
|                 MR.strings.onboarding_storage_info, | ||||
|                 stringResource(MR.strings.app_name), | ||||
|                 SettingsDataScreen.storageLocationText(storagePref), | ||||
|             ), | ||||
|         ) | ||||
|  | ||||
|         Button( | ||||
|             modifier = Modifier.fillMaxWidth(), | ||||
|             onClick = { | ||||
|                 try { | ||||
|                     pickStorageLocation.launch(null) | ||||
|                 } catch (e: ActivityNotFoundException) { | ||||
|                     context.toast(MR.strings.file_picker_error) | ||||
|                 } | ||||
|             }, | ||||
|         ) { | ||||
|             Text(stringResource(MR.strings.onboarding_storage_action_select)) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,40 @@ | ||||
| package eu.kanade.presentation.more.onboarding | ||||
|  | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.getValue | ||||
| import eu.kanade.domain.ui.UiPreferences | ||||
| import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode | ||||
| import eu.kanade.presentation.more.settings.widget.AppThemeModePreferenceWidget | ||||
| import eu.kanade.presentation.more.settings.widget.AppThemePreferenceWidget | ||||
| import tachiyomi.presentation.core.util.collectAsState | ||||
|  | ||||
| @Composable | ||||
| internal fun ThemeStep( | ||||
|     uiPreferences: UiPreferences, | ||||
| ) { | ||||
|     val themeModePref = uiPreferences.themeMode() | ||||
|     val themeMode by themeModePref.collectAsState() | ||||
|  | ||||
|     val appThemePref = uiPreferences.appTheme() | ||||
|     val appTheme by appThemePref.collectAsState() | ||||
|  | ||||
|     val amoledPref = uiPreferences.themeDarkAmoled() | ||||
|     val amoled by amoledPref.collectAsState() | ||||
|  | ||||
|     Column { | ||||
|         AppThemeModePreferenceWidget( | ||||
|             value = themeMode, | ||||
|             onItemClick = { | ||||
|                 themeModePref.set(it) | ||||
|                 setAppCompatDelegateThemeMode(it) | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|         AppThemePreferenceWidget( | ||||
|             value = appTheme, | ||||
|             amoled = amoled, | ||||
|             onItemClick = { appThemePref.set(it) }, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -43,6 +43,7 @@ import eu.kanade.tachiyomi.network.PREF_DOH_NJALLA | ||||
| import eu.kanade.tachiyomi.network.PREF_DOH_QUAD101 | ||||
| import eu.kanade.tachiyomi.network.PREF_DOH_QUAD9 | ||||
| import eu.kanade.tachiyomi.network.PREF_DOH_SHECAN | ||||
| import eu.kanade.tachiyomi.ui.more.OnboardingScreen | ||||
| import eu.kanade.tachiyomi.util.CrashLogUtil | ||||
| import eu.kanade.tachiyomi.util.system.isPreviewBuildType | ||||
| import eu.kanade.tachiyomi.util.system.isReleaseBuildType | ||||
| @@ -110,6 +111,10 @@ object SettingsAdvancedScreen : SearchableSettings { | ||||
|                         title = stringResource(MR.strings.pref_debug_info), | ||||
|                         onClick = { navigator.push(DebugInfoScreen()) }, | ||||
|                     ), | ||||
|                     Preference.PreferenceItem.TextPreference( | ||||
|                         title = stringResource(MR.strings.pref_onboarding_guide), | ||||
|                         onClick = { navigator.push(OnboardingScreen()) }, | ||||
|                     ), | ||||
|                 ), | ||||
|             ) | ||||
|             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||||
|   | ||||
| @@ -2,8 +2,8 @@ package eu.kanade.presentation.more.settings.screen | ||||
|  | ||||
| import android.app.Activity | ||||
| import android.content.Context | ||||
| import android.os.Build | ||||
| import androidx.appcompat.app.AppCompatDelegate | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.LaunchedEffect | ||||
| import androidx.compose.runtime.ReadOnlyComposable | ||||
| @@ -19,13 +19,11 @@ import eu.kanade.domain.ui.model.TabletUiMode | ||||
| import eu.kanade.domain.ui.model.ThemeMode | ||||
| import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode | ||||
| import eu.kanade.presentation.more.settings.Preference | ||||
| import eu.kanade.presentation.more.settings.widget.AppThemeModePreferenceWidget | ||||
| import eu.kanade.presentation.more.settings.widget.AppThemePreferenceWidget | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.util.system.LocaleHelper | ||||
| import eu.kanade.tachiyomi.util.system.toast | ||||
| import kotlinx.coroutines.flow.collectLatest | ||||
| import kotlinx.coroutines.flow.drop | ||||
| import kotlinx.coroutines.flow.merge | ||||
| import org.xmlpull.v1.XmlPullParser | ||||
| import tachiyomi.core.i18n.stringResource | ||||
| import tachiyomi.i18n.MR | ||||
| @@ -33,7 +31,7 @@ import tachiyomi.presentation.core.i18n.stringResource | ||||
| import tachiyomi.presentation.core.util.collectAsState | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.util.Date | ||||
| import java.time.Instant | ||||
|  | ||||
| object SettingsAppearanceScreen : SearchableSettings { | ||||
|  | ||||
| @@ -43,72 +41,59 @@ object SettingsAppearanceScreen : SearchableSettings { | ||||
|  | ||||
|     @Composable | ||||
|     override fun getPreferences(): List<Preference> { | ||||
|         val context = LocalContext.current | ||||
|         val uiPreferences = remember { Injekt.get<UiPreferences>() } | ||||
|  | ||||
|         return listOf( | ||||
|             getThemeGroup(context = context, uiPreferences = uiPreferences), | ||||
|             getDisplayGroup(context = context, uiPreferences = uiPreferences), | ||||
|             getThemeGroup(uiPreferences = uiPreferences), | ||||
|             getDisplayGroup(uiPreferences = uiPreferences), | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     @Composable | ||||
|     private fun getThemeGroup( | ||||
|         context: Context, | ||||
|         uiPreferences: UiPreferences, | ||||
|     ): Preference.PreferenceGroup { | ||||
|         val context = LocalContext.current | ||||
|  | ||||
|         val themeModePref = uiPreferences.themeMode() | ||||
|         val themeMode by themeModePref.collectAsState() | ||||
|  | ||||
|         val appThemePref = uiPreferences.appTheme() | ||||
|         val appTheme by appThemePref.collectAsState() | ||||
|  | ||||
|         val amoledPref = uiPreferences.themeDarkAmoled() | ||||
|         val amoled by amoledPref.collectAsState() | ||||
|  | ||||
|         LaunchedEffect(themeMode) { | ||||
|             setAppCompatDelegateThemeMode(themeMode) | ||||
|         } | ||||
|  | ||||
|         LaunchedEffect(Unit) { | ||||
|             merge(appThemePref.changes(), amoledPref.changes()) | ||||
|                 .drop(2) | ||||
|                 .collectLatest { (context as? Activity)?.let { ActivityCompat.recreate(it) } } | ||||
|         } | ||||
|  | ||||
|         return Preference.PreferenceGroup( | ||||
|             title = stringResource(MR.strings.pref_category_theme), | ||||
|             preferenceItems = listOf( | ||||
|                 Preference.PreferenceItem.ListPreference( | ||||
|                     pref = themeModePref, | ||||
|                     title = stringResource(MR.strings.pref_theme_mode), | ||||
|                     entries = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||||
|                         mapOf( | ||||
|                             ThemeMode.SYSTEM to stringResource(MR.strings.theme_system), | ||||
|                             ThemeMode.LIGHT to stringResource(MR.strings.theme_light), | ||||
|                             ThemeMode.DARK to stringResource(MR.strings.theme_dark), | ||||
|                         ) | ||||
|                     } else { | ||||
|                         mapOf( | ||||
|                             ThemeMode.LIGHT to stringResource(MR.strings.theme_light), | ||||
|                             ThemeMode.DARK to stringResource(MR.strings.theme_dark), | ||||
|                         ) | ||||
|                     }, | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.CustomPreference( | ||||
|                     title = stringResource(MR.strings.pref_app_theme), | ||||
|                 ) { item -> | ||||
|                     val value by appThemePref.collectAsState() | ||||
|                     AppThemePreferenceWidget( | ||||
|                         title = item.title, | ||||
|                         value = value, | ||||
|                         amoled = amoled, | ||||
|                         onItemClick = { appThemePref.set(it) }, | ||||
|                     ) | ||||
|                 ) { | ||||
|                     Column { | ||||
|                         AppThemeModePreferenceWidget( | ||||
|                             value = themeMode, | ||||
|                             onItemClick = { | ||||
|                                 themeModePref.set(it) | ||||
|                                 setAppCompatDelegateThemeMode(it) | ||||
|                             }, | ||||
|                         ) | ||||
|  | ||||
|                         AppThemePreferenceWidget( | ||||
|                             value = appTheme, | ||||
|                             amoled = amoled, | ||||
|                             onItemClick = { appThemePref.set(it) }, | ||||
|                         ) | ||||
|                     } | ||||
|                 }, | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     pref = amoledPref, | ||||
|                     title = stringResource(MR.strings.pref_dark_theme_pure_black), | ||||
|                     enabled = themeMode != ThemeMode.LIGHT, | ||||
|                     onValueChanged = { | ||||
|                         (context as? Activity)?.let { ActivityCompat.recreate(it) } | ||||
|                         true | ||||
|                     }, | ||||
|                 ), | ||||
|             ), | ||||
|         ) | ||||
| @@ -116,14 +101,15 @@ object SettingsAppearanceScreen : SearchableSettings { | ||||
|  | ||||
|     @Composable | ||||
|     private fun getDisplayGroup( | ||||
|         context: Context, | ||||
|         uiPreferences: UiPreferences, | ||||
|     ): Preference.PreferenceGroup { | ||||
|         val context = LocalContext.current | ||||
|  | ||||
|         val langs = remember { getLangs(context) } | ||||
|         var currentLanguage by remember { | ||||
|             mutableStateOf(AppCompatDelegate.getApplicationLocales().get(0)?.toLanguageTag() ?: "") | ||||
|         } | ||||
|         val now = remember { Date().time } | ||||
|         val now = remember { Instant.now().toEpochMilli() } | ||||
|  | ||||
|         val dateFormat by uiPreferences.dateFormat().collectAsState() | ||||
|         val formattedNow = remember(dateFormat) { | ||||
|   | ||||
| @@ -7,6 +7,7 @@ import android.net.Uri | ||||
| import android.os.Environment | ||||
| import android.text.format.Formatter | ||||
| import android.widget.Toast | ||||
| import androidx.activity.compose.ManagedActivityResultLauncher | ||||
| import androidx.activity.compose.rememberLauncherForActivityResult | ||||
| import androidx.activity.result.contract.ActivityResultContracts | ||||
| import androidx.compose.foundation.layout.Box | ||||
| @@ -89,13 +90,12 @@ object SettingsDataScreen : SearchableSettings { | ||||
|     } | ||||
|  | ||||
|     @Composable | ||||
|     private fun getStorageLocationPref( | ||||
|         storagePreferences: StoragePreferences, | ||||
|     ): Preference.PreferenceItem.TextPreference { | ||||
|     fun storageLocationPicker( | ||||
|         storageDirPref: tachiyomi.core.preference.Preference<String>, | ||||
|     ): ManagedActivityResultLauncher<Uri?, Uri?> { | ||||
|         val context = LocalContext.current | ||||
|         val storageDirPref = storagePreferences.baseStorageDirectory() | ||||
|         val storageDir by storageDirPref.collectAsState() | ||||
|         val pickStorageLocation = rememberLauncherForActivityResult( | ||||
|  | ||||
|         return rememberLauncherForActivityResult( | ||||
|             contract = ActivityResultContracts.OpenDocumentTree(), | ||||
|         ) { uri -> | ||||
|             if (uri != null) { | ||||
| @@ -110,13 +110,35 @@ object SettingsDataScreen : SearchableSettings { | ||||
|                 Injekt.get<DownloadCache>().invalidateCache() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Composable | ||||
|     fun storageLocationText( | ||||
|         storageDirPref: tachiyomi.core.preference.Preference<String>, | ||||
|     ): String { | ||||
|         val context = LocalContext.current | ||||
|         val storageDir by storageDirPref.collectAsState() | ||||
|  | ||||
|         if (storageDir == storageDirPref.defaultValue()) { | ||||
|             return stringResource(MR.strings.no_location_set) | ||||
|         } | ||||
|  | ||||
|         return remember(storageDir) { | ||||
|             val file = UniFile.fromUri(context, storageDir.toUri()) | ||||
|             file?.filePath ?: file?.uri?.toString() | ||||
|         } ?: stringResource(MR.strings.invalid_location, storageDir) | ||||
|     } | ||||
|  | ||||
|     @Composable | ||||
|     private fun getStorageLocationPref( | ||||
|         storagePreferences: StoragePreferences, | ||||
|     ): Preference.PreferenceItem.TextPreference { | ||||
|         val context = LocalContext.current | ||||
|         val pickStorageLocation = storageLocationPicker(storagePreferences.baseStorageDirectory()) | ||||
|  | ||||
|         return Preference.PreferenceItem.TextPreference( | ||||
|             title = stringResource(MR.strings.pref_storage_location), | ||||
|             subtitle = remember(storageDir) { | ||||
|                 val file = UniFile.fromUri(context, storageDir.toUri()) | ||||
|                 file?.filePath ?: file?.uri?.toString() | ||||
|             } ?: stringResource(MR.strings.invalid_location, storageDir), | ||||
|             subtitle = storageLocationText(storagePreferences.baseStorageDirectory()), | ||||
|             onClick = { | ||||
|                 try { | ||||
|                     pickStorageLocation.launch(null) | ||||
|   | ||||
| @@ -0,0 +1,56 @@ | ||||
| package eu.kanade.presentation.more.settings.widget | ||||
|  | ||||
| import android.os.Build | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.material3.MultiChoiceSegmentedButtonRow | ||||
| import androidx.compose.material3.SegmentedButton | ||||
| import androidx.compose.material3.SegmentedButtonDefaults | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Modifier | ||||
| import eu.kanade.domain.ui.model.ThemeMode | ||||
| import tachiyomi.i18n.MR | ||||
| import tachiyomi.presentation.core.i18n.stringResource | ||||
|  | ||||
| private val options = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||||
|     mapOf( | ||||
|         ThemeMode.SYSTEM to MR.strings.theme_system, | ||||
|         ThemeMode.LIGHT to MR.strings.theme_light, | ||||
|         ThemeMode.DARK to MR.strings.theme_dark, | ||||
|     ) | ||||
| } else { | ||||
|     mapOf( | ||||
|         ThemeMode.LIGHT to MR.strings.theme_light, | ||||
|         ThemeMode.DARK to MR.strings.theme_dark, | ||||
|     ) | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| internal fun AppThemeModePreferenceWidget( | ||||
|     value: ThemeMode, | ||||
|     onItemClick: (ThemeMode) -> Unit, | ||||
| ) { | ||||
|     BasePreferenceWidget( | ||||
|         subcomponent = { | ||||
|             MultiChoiceSegmentedButtonRow( | ||||
|                 modifier = Modifier | ||||
|                     .fillMaxWidth() | ||||
|                     .padding(horizontal = PrefsHorizontalPadding), | ||||
|             ) { | ||||
|                 options.onEachIndexed { index, (mode, labelRes) -> | ||||
|                     SegmentedButton( | ||||
|                         checked = mode == value, | ||||
|                         onCheckedChange = { onItemClick(mode) }, | ||||
|                         shape = SegmentedButtonDefaults.itemShape( | ||||
|                             index, | ||||
|                             options.size, | ||||
|                         ), | ||||
|                     ) { | ||||
|                         Text(stringResource(labelRes)) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|     ) | ||||
| } | ||||
| @@ -1,5 +1,6 @@ | ||||
| package eu.kanade.presentation.more.settings.widget | ||||
|  | ||||
| import android.app.Activity | ||||
| import androidx.compose.foundation.background | ||||
| import androidx.compose.foundation.border | ||||
| import androidx.compose.foundation.clickable | ||||
| @@ -36,9 +37,11 @@ import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.draw.alpha | ||||
| import androidx.compose.ui.draw.clip | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import androidx.compose.ui.text.style.TextAlign | ||||
| import androidx.compose.ui.tooling.preview.PreviewLightDark | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.core.app.ActivityCompat | ||||
| import eu.kanade.domain.ui.model.AppTheme | ||||
| import eu.kanade.presentation.manga.components.MangaCover | ||||
| import eu.kanade.presentation.theme.TachiyomiTheme | ||||
| @@ -51,13 +54,11 @@ import tachiyomi.presentation.core.util.secondaryItemAlpha | ||||
|  | ||||
| @Composable | ||||
| internal fun AppThemePreferenceWidget( | ||||
|     title: String, | ||||
|     value: AppTheme, | ||||
|     amoled: Boolean, | ||||
|     onItemClick: (AppTheme) -> Unit, | ||||
| ) { | ||||
|     BasePreferenceWidget( | ||||
|         title = title, | ||||
|         subcomponent = { | ||||
|             AppThemesList( | ||||
|                 currentTheme = value, | ||||
| @@ -74,6 +75,7 @@ private fun AppThemesList( | ||||
|     amoled: Boolean, | ||||
|     onItemClick: (AppTheme) -> Unit, | ||||
| ) { | ||||
|     val context = LocalContext.current | ||||
|     val appThemes = remember { | ||||
|         AppTheme.entries | ||||
|             .filterNot { it.titleRes == null || (it == AppTheme.MONET && !DeviceUtil.isDynamicColorAvailable) } | ||||
| @@ -97,7 +99,10 @@ private fun AppThemesList( | ||||
|                 ) { | ||||
|                     AppThemePreviewItem( | ||||
|                         selected = currentTheme == appTheme, | ||||
|                         onClick = { onItemClick(appTheme) }, | ||||
|                         onClick = { | ||||
|                             onItemClick(appTheme) | ||||
|                             (context as? Activity)?.let { ActivityCompat.recreate(it) } | ||||
|                         }, | ||||
|                     ) | ||||
|                 } | ||||
|  | ||||
|   | ||||
| @@ -7,7 +7,7 @@ import androidx.compose.runtime.ReadOnlyComposable | ||||
| import tachiyomi.core.i18n.stringResource | ||||
| import tachiyomi.i18n.MR | ||||
| import tachiyomi.presentation.core.i18n.stringResource | ||||
| import java.util.Date | ||||
| import java.time.Instant | ||||
| import kotlin.time.Duration | ||||
| import kotlin.time.Duration.Companion.minutes | ||||
|  | ||||
| @@ -29,7 +29,7 @@ fun Duration.toDurationString(context: Context, fallback: String): String { | ||||
| @Composable | ||||
| @ReadOnlyComposable | ||||
| fun relativeTimeSpanString(epochMillis: Long): String { | ||||
|     val now = Date().time | ||||
|     val now = Instant.now().toEpochMilli() | ||||
|     return when { | ||||
|         epochMillis <= 0L -> stringResource(MR.strings.relative_time_span_never) | ||||
|         now - epochMillis < 1.minutes.inWholeMilliseconds -> stringResource( | ||||
|   | ||||
| @@ -396,7 +396,12 @@ object Migrations { | ||||
|                     newKey = { Preference.privateKey(it) }, | ||||
|                 ) | ||||
|             } | ||||
|             if (oldVersion < 110) { | ||||
|             if (oldVersion < 111) { | ||||
|                 File(context.cacheDir, "dl_index_cache") | ||||
|                     .takeIf { it.exists() } | ||||
|                     ?.delete() | ||||
|             } | ||||
|             if (oldVersion < 112) { | ||||
|                 val prefsToReplace = listOf( | ||||
|                     "pref_download_only", | ||||
|                     "incognito_mode", | ||||
| @@ -409,6 +414,7 @@ object Migrations { | ||||
|                     "last_app_check", | ||||
|                     "last_ext_check", | ||||
|                     "last_version_code", | ||||
|                     "storage_dir", | ||||
|                 ) | ||||
|                 replacePreferences( | ||||
|                     preferenceStore = preferenceStore, | ||||
| @@ -416,11 +422,6 @@ object Migrations { | ||||
|                     newKey = { Preference.appStateKey(it) }, | ||||
|                 ) | ||||
|             } | ||||
|             if (oldVersion < 111) { | ||||
|                 File(context.cacheDir, "dl_index_cache") | ||||
|                     .takeIf { it.exists() } | ||||
|                     ?.delete() | ||||
|             } | ||||
|             return true | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -25,10 +25,8 @@ import tachiyomi.domain.backup.service.BackupPreferences | ||||
| import tachiyomi.domain.storage.service.StorageManager | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.util.Date | ||||
| import java.time.Instant | ||||
| import java.util.concurrent.TimeUnit | ||||
| import kotlin.time.Duration.Companion.minutes | ||||
| import kotlin.time.toJavaDuration | ||||
|  | ||||
| class BackupCreateJob(private val context: Context, workerParams: WorkerParameters) : | ||||
|     CoroutineWorker(context, workerParams) { | ||||
| @@ -52,7 +50,7 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete | ||||
|         return try { | ||||
|             val location = BackupCreator(context).createBackup(uri, flags, isAutoBackup) | ||||
|             if (isAutoBackup) { | ||||
|                 backupPreferences.lastAutoBackupTimestamp().set(Date().time) | ||||
|                 backupPreferences.lastAutoBackupTimestamp().set(Instant.now().toEpochMilli()) | ||||
|             } else { | ||||
|                 notifier.showBackupComplete(UniFile.fromUri(context, location.toUri())!!) | ||||
|             } | ||||
| @@ -97,7 +95,7 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete | ||||
|                     10, | ||||
|                     TimeUnit.MINUTES, | ||||
|                 ) | ||||
|                     .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10.minutes.toJavaDuration()) | ||||
|                     .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, TimeUnit.MINUTES) | ||||
|                     .addTag(TAG_AUTO) | ||||
|                     .setConstraints(constraints) | ||||
|                     .setInputData(workDataOf(IS_AUTO_BACKUP_KEY to true)) | ||||
|   | ||||
| @@ -21,7 +21,7 @@ import eu.kanade.tachiyomi.source.sourcePreferences | ||||
| import eu.kanade.tachiyomi.util.BackupUtil | ||||
| import eu.kanade.tachiyomi.util.system.createFileInCacheDir | ||||
| import kotlinx.coroutines.coroutineScope | ||||
| import kotlinx.coroutines.isActive | ||||
| import kotlinx.coroutines.ensureActive | ||||
| import tachiyomi.core.i18n.stringResource | ||||
| import tachiyomi.core.preference.AndroidPreferenceStore | ||||
| import tachiyomi.core.preference.PreferenceStore | ||||
| @@ -76,14 +76,12 @@ class BackupRestorer( | ||||
|  | ||||
|     private val errors = mutableListOf<Pair<Date, String>>() | ||||
|  | ||||
|     suspend fun syncFromBackup(uri: Uri, sync: Boolean): Boolean { | ||||
|     suspend fun syncFromBackup(uri: Uri, sync: Boolean) { | ||||
|         val startTime = System.currentTimeMillis() | ||||
|         restoreProgress = 0 | ||||
|         errors.clear() | ||||
|  | ||||
|         if (!performRestore(uri, sync)) { | ||||
|             return false | ||||
|         } | ||||
|         performRestore(uri, sync) | ||||
|  | ||||
|         val endTime = System.currentTimeMillis() | ||||
|         val time = endTime - startTime | ||||
| @@ -95,7 +93,6 @@ class BackupRestorer( | ||||
|         } else { | ||||
|             notifier.showRestoreComplete(time, errors.size, logFile.parent, logFile.name) | ||||
|         } | ||||
|         return true | ||||
|     } | ||||
|  | ||||
|     private fun writeErrorLog(): File { | ||||
| @@ -117,74 +114,57 @@ class BackupRestorer( | ||||
|         return File("") | ||||
|     } | ||||
|  | ||||
|     private suspend fun performRestore(uri: Uri, sync: Boolean): Boolean { | ||||
|     private suspend fun performRestore(uri: Uri, sync: Boolean) { | ||||
|         val backup = BackupUtil.decodeBackup(context, uri) | ||||
|  | ||||
|         restoreAmount = backup.backupManga.size + 3 // +3 for categories, app prefs, source prefs | ||||
|  | ||||
|         // Restore categories | ||||
|         if (backup.backupCategories.isNotEmpty()) { | ||||
|             restoreCategories(backup.backupCategories) | ||||
|         } | ||||
|  | ||||
|         // Store source mapping for error messages | ||||
|         val backupMaps = backup.backupBrokenSources.map { BackupSource(it.name, it.sourceId) } + backup.backupSources | ||||
|         sourceMapping = backupMaps.associate { it.sourceId to it.name } | ||||
|         now = ZonedDateTime.now() | ||||
|         currentFetchWindow = fetchInterval.getWindow(now) | ||||
|  | ||||
|         return coroutineScope { | ||||
|         coroutineScope { | ||||
|             ensureActive() | ||||
|             restoreCategories(backup.backupCategories) | ||||
|  | ||||
|             ensureActive() | ||||
|             restoreAppPreferences(backup.backupPreferences) | ||||
|  | ||||
|             ensureActive() | ||||
|             restoreSourcePreferences(backup.backupSourcePreferences) | ||||
|  | ||||
|             // Restore individual manga | ||||
|             backup.backupManga.forEach { | ||||
|                 if (!isActive) { | ||||
|                     return@coroutineScope false | ||||
|                 } | ||||
|  | ||||
|                 ensureActive() | ||||
|                 restoreManga(it, backup.backupCategories, sync) | ||||
|             } | ||||
|             // TODO: optionally trigger online library + tracker update | ||||
|  | ||||
|             true | ||||
|             // TODO: optionally trigger online library + tracker update | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private suspend fun restoreCategories(backupCategories: List<BackupCategory>) { | ||||
|         // Get categories from file and from db | ||||
|         val dbCategories = getCategories.await() | ||||
|         if (backupCategories.isNotEmpty()) { | ||||
|             val dbCategories = getCategories.await() | ||||
|             val dbCategoriesByName = dbCategories.associateBy { it.name } | ||||
|  | ||||
|         val categories = backupCategories.map { | ||||
|             var category = it.getCategory() | ||||
|             var found = false | ||||
|             for (dbCategory in dbCategories) { | ||||
|                 // If the category is already in the db, assign the id to the file's category | ||||
|                 // and do nothing | ||||
|                 if (category.name == dbCategory.name) { | ||||
|                     category = category.copy(id = dbCategory.id) | ||||
|                     found = true | ||||
|                     break | ||||
|                 } | ||||
|             } | ||||
|             if (!found) { | ||||
|                 // Let the db assign the id | ||||
|                 val id = handler.awaitOneExecutable { | ||||
|                     categoriesQueries.insert(category.name, category.order, category.flags) | ||||
|                     categoriesQueries.selectLastInsertedRowId() | ||||
|                 } | ||||
|                 category = category.copy(id = id) | ||||
|             val categories = backupCategories.map { | ||||
|                 dbCategoriesByName[it.name] | ||||
|                     ?: handler.awaitOneExecutable { | ||||
|                         categoriesQueries.insert(it.name, it.order, it.flags) | ||||
|                         categoriesQueries.selectLastInsertedRowId() | ||||
|                     }.let { id -> it.toCategory(id) } | ||||
|             } | ||||
|  | ||||
|             category | ||||
|             libraryPreferences.categorizedDisplaySettings().set( | ||||
|                 (dbCategories + categories) | ||||
|                     .distinctBy { it.flags } | ||||
|                     .size > 1, | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|         libraryPreferences.categorizedDisplaySettings().set( | ||||
|             (dbCategories + categories) | ||||
|                 .distinctBy { it.flags } | ||||
|                 .size > 1, | ||||
|         ) | ||||
|  | ||||
|         restoreProgress += 1 | ||||
|         showRestoreProgress( | ||||
|             restoreProgress, | ||||
|   | ||||
| @@ -11,14 +11,12 @@ class BackupCategory( | ||||
|     // @ProtoNumber(3) val updateInterval: Int = 0, 1.x value not used in 0.x | ||||
|     @ProtoNumber(100) var flags: Long = 0, | ||||
| ) { | ||||
|     fun getCategory(): Category { | ||||
|         return Category( | ||||
|             id = 0, | ||||
|             name = this@BackupCategory.name, | ||||
|             flags = this@BackupCategory.flags, | ||||
|             order = this@BackupCategory.order, | ||||
|         ) | ||||
|     } | ||||
|     fun toCategory(id: Long) = Category( | ||||
|         id = id, | ||||
|         name = this@BackupCategory.name, | ||||
|         flags = this@BackupCategory.flags, | ||||
|         order = this@BackupCategory.order, | ||||
|     ) | ||||
| } | ||||
|  | ||||
| val backupCategoryMapper = { category: Category -> | ||||
|   | ||||
| @@ -47,7 +47,6 @@ import tachiyomi.core.storage.extension | ||||
| import tachiyomi.core.util.lang.launchIO | ||||
| import tachiyomi.core.util.lang.launchNow | ||||
| import tachiyomi.core.util.lang.withIOContext | ||||
| import tachiyomi.core.util.lang.withUIContext | ||||
| import tachiyomi.core.util.system.ImageUtil | ||||
| import tachiyomi.core.util.system.logcat | ||||
| import tachiyomi.domain.category.interactor.GetCategories | ||||
| @@ -265,24 +264,21 @@ class Downloader( | ||||
|      * @param chapters the list of chapters to download. | ||||
|      * @param autoStart whether to start the downloader after enqueing the chapters. | ||||
|      */ | ||||
|     fun queueChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean) = launchIO { | ||||
|         if (chapters.isEmpty()) { | ||||
|             return@launchIO | ||||
|         } | ||||
|     fun queueChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean) { | ||||
|         if (chapters.isEmpty()) return | ||||
|  | ||||
|         val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchIO | ||||
|         val source = sourceManager.get(manga.source) as? HttpSource ?: return | ||||
|         val wasEmpty = queueState.value.isEmpty() | ||||
|         val chaptersWithoutDir = chapters | ||||
|         val chaptersToQueue = chapters.asSequence() | ||||
|             // Filter out those already downloaded. | ||||
|             .filter { provider.findChapterDir(it.name, it.scanlator, manga.title, source) == null } | ||||
|             // Add chapters to queue from the start. | ||||
|             .sortedByDescending { it.sourceOrder } | ||||
|  | ||||
|         val chaptersToQueue = chaptersWithoutDir | ||||
|             // Filter out those already enqueued. | ||||
|             .filter { chapter -> queueState.value.none { it.chapter.id == chapter.id } } | ||||
|             // Create a download for each one. | ||||
|             .map { Download(source, manga, it) } | ||||
|             .toList() | ||||
|  | ||||
|         if (chaptersToQueue.isNotEmpty()) { | ||||
|             addAllToQueue(chaptersToQueue) | ||||
| @@ -299,13 +295,11 @@ class Downloader( | ||||
|                     queuedDownloads > DOWNLOADS_QUEUED_WARNING_THRESHOLD || | ||||
|                     maxDownloadsFromSource > CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD | ||||
|                 ) { | ||||
|                     withUIContext { | ||||
|                         notifier.onWarning( | ||||
|                             context.stringResource(MR.strings.download_queue_size_warning), | ||||
|                             WARNING_NOTIF_TIMEOUT_MS, | ||||
|                             NotificationHandler.openUrl(context, LibraryUpdateNotifier.HELP_WARNING_URL), | ||||
|                         ) | ||||
|                     } | ||||
|                     notifier.onWarning( | ||||
|                         context.stringResource(MR.strings.download_queue_size_warning), | ||||
|                         WARNING_NOTIF_TIMEOUT_MS, | ||||
|                         NotificationHandler.openUrl(context, LibraryUpdateNotifier.HELP_WARNING_URL), | ||||
|                     ) | ||||
|                 } | ||||
|                 DownloadJob.start(context) | ||||
|             } | ||||
|   | ||||
| @@ -69,8 +69,8 @@ import tachiyomi.i18n.MR | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.io.File | ||||
| import java.time.Instant | ||||
| import java.time.ZonedDateTime | ||||
| import java.util.Date | ||||
| import java.util.concurrent.CopyOnWriteArrayList | ||||
| import java.util.concurrent.TimeUnit | ||||
| import java.util.concurrent.atomic.AtomicBoolean | ||||
| @@ -111,7 +111,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet | ||||
|  | ||||
|         setForegroundSafely() | ||||
|  | ||||
|         libraryPreferences.lastUpdatedTimestamp().set(Date().time) | ||||
|         libraryPreferences.lastUpdatedTimestamp().set(Instant.now().toEpochMilli()) | ||||
|  | ||||
|         val categoryId = inputData.getLong(KEY_CATEGORY, -1L) | ||||
|         addMangaToQueue(categoryId) | ||||
|   | ||||
| @@ -7,7 +7,6 @@ import android.content.Intent | ||||
| import android.net.Uri | ||||
| import android.os.Build | ||||
| import androidx.core.net.toUri | ||||
| import com.hippo.unifile.UniFile | ||||
| import eu.kanade.tachiyomi.data.backup.BackupRestoreJob | ||||
| import eu.kanade.tachiyomi.data.download.DownloadManager | ||||
| import eu.kanade.tachiyomi.data.library.LibraryUpdateJob | ||||
| @@ -15,7 +14,6 @@ import eu.kanade.tachiyomi.data.sync.SyncDataJob | ||||
| import eu.kanade.tachiyomi.data.updater.AppUpdateDownloadJob | ||||
| import eu.kanade.tachiyomi.ui.main.MainActivity | ||||
| import eu.kanade.tachiyomi.ui.reader.ReaderActivity | ||||
| import eu.kanade.tachiyomi.util.storage.DiskUtil | ||||
| import eu.kanade.tachiyomi.util.system.cancelNotification | ||||
| import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat | ||||
| import eu.kanade.tachiyomi.util.system.notificationManager | ||||
| @@ -66,12 +64,6 @@ class NotificationReceiver : BroadcastReceiver() { | ||||
|                     context, | ||||
|                     intent.getStringExtra(EXTRA_URI)!!.toUri(), | ||||
|                 ) | ||||
|             // Delete image from path and dismiss notification | ||||
|             ACTION_DELETE_IMAGE -> | ||||
|                 deleteImage( | ||||
|                     context, | ||||
|                     intent.getStringExtra(EXTRA_URI)!!.toUri(), | ||||
|                 ) | ||||
|             // Share backup file | ||||
|             ACTION_SHARE_BACKUP -> | ||||
|                 shareFile( | ||||
| @@ -172,16 +164,6 @@ class NotificationReceiver : BroadcastReceiver() { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Called to delete image | ||||
|      * | ||||
|      * @param uri path of file | ||||
|      */ | ||||
|     private fun deleteImage(context: Context, uri: Uri) { | ||||
|         UniFile.fromUri(context, uri)?.delete() | ||||
|         DiskUtil.scanMedia(context, uri) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Method called when user wants to stop a backup restore job. | ||||
|      * | ||||
| @@ -429,26 +411,6 @@ class NotificationReceiver : BroadcastReceiver() { | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * Returns [PendingIntent] that starts a service which removes an image from disk | ||||
|          * | ||||
|          * @param context context of application | ||||
|          * @param uri location path of file | ||||
|          * @return [PendingIntent] | ||||
|          */ | ||||
|         internal fun deleteImagePendingBroadcast(context: Context, uri: Uri): PendingIntent { | ||||
|             val intent = Intent(context, NotificationReceiver::class.java).apply { | ||||
|                 action = ACTION_DELETE_IMAGE | ||||
|                 putExtra(EXTRA_URI, uri.toString()) | ||||
|             } | ||||
|             return PendingIntent.getBroadcast( | ||||
|                 context, | ||||
|                 0, | ||||
|                 intent, | ||||
|                 PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * Returns [PendingIntent] that starts a reader activity containing chapter. | ||||
|          * | ||||
|   | ||||
| @@ -23,7 +23,7 @@ import java.io.ByteArrayInputStream | ||||
| import java.io.ByteArrayOutputStream | ||||
| import java.io.File | ||||
| import java.io.InputStream | ||||
| import java.util.Date | ||||
| import java.time.Instant | ||||
|  | ||||
| class ImageSaver( | ||||
|     val context: Context, | ||||
| @@ -79,7 +79,7 @@ class ImageSaver( | ||||
|             MediaStore.Images.Media.RELATIVE_PATH to relativePath, | ||||
|             MediaStore.Images.Media.DISPLAY_NAME to image.name, | ||||
|             MediaStore.Images.Media.MIME_TYPE to type.mime, | ||||
|             MediaStore.Images.Media.DATE_MODIFIED to Date().time * 1000, | ||||
|             MediaStore.Images.Media.DATE_MODIFIED to Instant.now().toEpochMilli(), | ||||
|         ) | ||||
|  | ||||
|         val picture = findUriOrDefault(relativePath, filename) { | ||||
|   | ||||
| @@ -26,7 +26,10 @@ import okhttp3.OkHttpClient | ||||
| import okhttp3.RequestBody.Companion.toRequestBody | ||||
| import tachiyomi.core.util.lang.withIOContext | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.util.Calendar | ||||
| import java.time.Instant | ||||
| import java.time.LocalDate | ||||
| import java.time.ZoneId | ||||
| import java.time.ZonedDateTime | ||||
| import kotlin.time.Duration.Companion.minutes | ||||
|  | ||||
| class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { | ||||
| @@ -328,13 +331,15 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { | ||||
|  | ||||
|     private fun parseDate(struct: JsonObject, dateKey: String): Long { | ||||
|         return try { | ||||
|             val date = Calendar.getInstance() | ||||
|             date.set( | ||||
|                 struct[dateKey]!!.jsonObject["year"]!!.jsonPrimitive.int, | ||||
|                 struct[dateKey]!!.jsonObject["month"]!!.jsonPrimitive.int - 1, | ||||
|                 struct[dateKey]!!.jsonObject["day"]!!.jsonPrimitive.int, | ||||
|             ) | ||||
|             date.timeInMillis | ||||
|             return LocalDate | ||||
|                 .of( | ||||
|                     struct[dateKey]!!.jsonObject["year"]!!.jsonPrimitive.int, | ||||
|                     struct[dateKey]!!.jsonObject["month"]!!.jsonPrimitive.int, | ||||
|                     struct[dateKey]!!.jsonObject["day"]!!.jsonPrimitive.int, | ||||
|                 ) | ||||
|                 .atStartOfDay(ZoneId.systemDefault()) | ||||
|                 .toInstant() | ||||
|                 .toEpochMilli() | ||||
|         } catch (_: Exception) { | ||||
|             0L | ||||
|         } | ||||
| @@ -349,12 +354,11 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         val calendar = Calendar.getInstance() | ||||
|         calendar.timeInMillis = dateValue | ||||
|         val dateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(dateValue), ZoneId.systemDefault()) | ||||
|         return buildJsonObject { | ||||
|             put("year", calendar.get(Calendar.YEAR)) | ||||
|             put("month", calendar.get(Calendar.MONTH) + 1) | ||||
|             put("day", calendar.get(Calendar.DAY_OF_MONTH)) | ||||
|             put("year", dateTime.year) | ||||
|             put("month", dateTime.monthValue) | ||||
|             put("day", dateTime.dayOfMonth) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -17,7 +17,7 @@ import tachiyomi.core.preference.PreferenceStore | ||||
| import tachiyomi.core.util.lang.withIOContext | ||||
| import tachiyomi.core.util.system.logcat | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.util.Date | ||||
| import java.time.Instant | ||||
| import kotlin.time.Duration.Companion.days | ||||
|  | ||||
| internal class ExtensionGithubApi { | ||||
| @@ -76,14 +76,16 @@ internal class ExtensionGithubApi { | ||||
|         fromAvailableExtensionList: Boolean = false, | ||||
|     ): List<Extension.Installed>? { | ||||
|         // Limit checks to once a day at most | ||||
|         if (!fromAvailableExtensionList && Date().time < lastExtCheck.get() + 1.days.inWholeMilliseconds) { | ||||
|         if (!fromAvailableExtensionList && | ||||
|             Instant.now().toEpochMilli() < lastExtCheck.get() + 1.days.inWholeMilliseconds | ||||
|         ) { | ||||
|             return null | ||||
|         } | ||||
|  | ||||
|         val extensions = if (fromAvailableExtensionList) { | ||||
|             extensionManager.availableExtensionsFlow.value | ||||
|         } else { | ||||
|             findExtensions().also { lastExtCheck.set(Date().time) } | ||||
|             findExtensions().also { lastExtCheck.set(Instant.now().toEpochMilli()) } | ||||
|         } | ||||
|  | ||||
|         val installedExtensions = ExtensionLoader.loadExtensions(context) | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import eu.kanade.presentation.components.AppBar | ||||
| import eu.kanade.presentation.components.TabContent | ||||
| import eu.kanade.tachiyomi.extension.model.Extension | ||||
| import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreen | ||||
| import eu.kanade.tachiyomi.ui.webview.WebViewScreen | ||||
| import kotlinx.collections.immutable.persistentListOf | ||||
| import tachiyomi.i18n.MR | ||||
| import tachiyomi.presentation.core.i18n.stringResource | ||||
| @@ -47,6 +48,17 @@ fun extensionsTab( | ||||
|                 }, | ||||
|                 onClickItemCancel = extensionsScreenModel::cancelInstallUpdateExtension, | ||||
|                 onClickUpdateAll = extensionsScreenModel::updateAllExtensions, | ||||
|                 onClickItemWebView = { extension -> | ||||
|                     extension.sources.getOrNull(0)?.let { | ||||
|                         navigator.push( | ||||
|                             WebViewScreen( | ||||
|                                 url = it.baseUrl, | ||||
|                                 initialTitle = it.name, | ||||
|                                 sourceId = it.id, | ||||
|                             ), | ||||
|                         ) | ||||
|                     } | ||||
|                 }, | ||||
|                 onInstallExtension = extensionsScreenModel::installExtension, | ||||
|                 onOpenExtension = { navigator.push(ExtensionDetailsScreen(it.pkgName)) }, | ||||
|                 onTrustExtension = { extensionsScreenModel.trustSignature(it.signatureHash) }, | ||||
|   | ||||
| @@ -53,7 +53,7 @@ import tachiyomi.presentation.core.i18n.stringResource | ||||
| import tachiyomi.presentation.core.screens.LoadingScreen | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.util.Date | ||||
| import java.time.Instant | ||||
|  | ||||
| @Composable | ||||
| internal fun MigrateDialog( | ||||
| @@ -298,7 +298,7 @@ internal class MigrateDialogScreenModel( | ||||
|                 favorite = true, | ||||
|                 chapterFlags = oldManga.chapterFlags, | ||||
|                 viewerFlags = oldManga.viewerFlags, | ||||
|                 dateAdded = if (replace) oldManga.dateAdded else Date().time, | ||||
|                 dateAdded = if (replace) oldManga.dateAdded else Instant.now().toEpochMilli(), | ||||
|             ), | ||||
|         ) | ||||
|     } | ||||
|   | ||||
| @@ -50,7 +50,7 @@ import tachiyomi.domain.source.interactor.GetRemoteManga | ||||
| import tachiyomi.domain.source.service.SourceManager | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.util.Date | ||||
| import java.time.Instant | ||||
| import eu.kanade.tachiyomi.source.model.Filter as SourceModelFilter | ||||
|  | ||||
| class BrowseSourceScreenModel( | ||||
| @@ -225,7 +225,7 @@ class BrowseSourceScreenModel( | ||||
|                 favorite = !manga.favorite, | ||||
|                 dateAdded = when (manga.favorite) { | ||||
|                     true -> 0 | ||||
|                     false -> Date().time | ||||
|                     false -> Instant.now().toEpochMilli() | ||||
|                 }, | ||||
|             ) | ||||
|  | ||||
|   | ||||
| @@ -126,12 +126,12 @@ object HomeScreen : Screen() { | ||||
|                                 materialFadeThroughIn(initialScale = 1f, durationMillis = TabFadeDuration) togetherWith | ||||
|                                     materialFadeThroughOut(durationMillis = TabFadeDuration) | ||||
|                             }, | ||||
|                             content = { | ||||
|                                 tabNavigator.saveableState(key = "currentTab", it) { | ||||
|                                     it.Content() | ||||
|                                 } | ||||
|                             }, | ||||
|                         ) | ||||
|                             label = "tabContent", | ||||
|                         ) { | ||||
|                             tabNavigator.saveableState(key = "currentTab", it) { | ||||
|                                 it.Content() | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|   | ||||
| @@ -33,6 +33,7 @@ import eu.kanade.presentation.library.LibrarySettingsDialog | ||||
| import eu.kanade.presentation.library.components.LibraryContent | ||||
| import eu.kanade.presentation.library.components.LibraryToolbar | ||||
| import eu.kanade.presentation.manga.components.LibraryBottomActionMenu | ||||
| import eu.kanade.presentation.more.onboarding.GETTING_STARTED_URL | ||||
| import eu.kanade.presentation.util.Tab | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.library.LibraryUpdateJob | ||||
| @@ -163,7 +164,7 @@ object LibraryTab : Tab { | ||||
|                             EmptyScreenAction( | ||||
|                                 stringRes = MR.strings.getting_started_guide, | ||||
|                                 icon = Icons.AutoMirrored.Outlined.HelpOutline, | ||||
|                                 onClick = { handler.openUri("https://tachiyomi.org/docs/guides/getting-started") }, | ||||
|                                 onClick = { handler.openUri(GETTING_STARTED_URL) }, | ||||
|                             ), | ||||
|                         ), | ||||
|                     ) | ||||
|   | ||||
| @@ -73,6 +73,7 @@ import eu.kanade.tachiyomi.ui.deeplink.DeepLinkScreen | ||||
| import eu.kanade.tachiyomi.ui.home.HomeScreen | ||||
| import eu.kanade.tachiyomi.ui.manga.MangaScreen | ||||
| import eu.kanade.tachiyomi.ui.more.NewUpdateScreen | ||||
| import eu.kanade.tachiyomi.ui.more.OnboardingScreen | ||||
| import eu.kanade.tachiyomi.util.system.dpToPx | ||||
| import eu.kanade.tachiyomi.util.system.isNavigationBarNeedsScrim | ||||
| import eu.kanade.tachiyomi.util.system.openInBrowser | ||||
| @@ -251,6 +252,7 @@ class MainActivity : BaseActivity() { | ||||
|                 HandleOnNewIntent(context = context, navigator = navigator) | ||||
|  | ||||
|                 CheckForUpdates() | ||||
|                 ShowOnboarding() | ||||
|             } | ||||
|  | ||||
|             var showChangelog by remember { mutableStateOf(didMigration && !BuildConfig.DEBUG) } | ||||
| @@ -342,6 +344,17 @@ class MainActivity : BaseActivity() { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Composable | ||||
|     private fun ShowOnboarding() { | ||||
|         val navigator = LocalNavigator.currentOrThrow | ||||
|  | ||||
|         LaunchedEffect(Unit) { | ||||
|             if (!preferences.shownOnboardingFlow().get()) { | ||||
|                 navigator.push(OnboardingScreen()) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Sets custom splash screen exit animation on devices prior to Android 12. | ||||
|      * | ||||
|   | ||||
| @@ -47,6 +47,7 @@ import eu.kanade.tachiyomi.ui.category.CategoryScreen | ||||
| import eu.kanade.tachiyomi.ui.home.HomeScreen | ||||
| import eu.kanade.tachiyomi.ui.manga.track.TrackInfoDialogHomeScreen | ||||
| import eu.kanade.tachiyomi.ui.reader.ReaderActivity | ||||
| import eu.kanade.tachiyomi.ui.setting.SettingsScreen | ||||
| import eu.kanade.tachiyomi.ui.webview.WebViewScreen | ||||
| import eu.kanade.tachiyomi.util.system.copyToClipboard | ||||
| import eu.kanade.tachiyomi.util.system.toShareIntent | ||||
| @@ -130,7 +131,13 @@ class MangaScreen( | ||||
|                     screenModel.source, | ||||
|                 ) | ||||
|             }.takeIf { isHttpSource }, | ||||
|             onTrackingClicked = screenModel::showTrackDialog.takeIf { successState.trackingAvailable }, | ||||
|             onTrackingClicked = { | ||||
|                 if (successState.trackingCount == 0) { | ||||
|                     navigator.push(SettingsScreen(SettingsScreen.Destination.Tracking)) | ||||
|                 } else { | ||||
|                     screenModel.showTrackDialog() | ||||
|                 } | ||||
|             }, | ||||
|             onTagSearch = { scope.launch { performGenreSearch(navigator, it, screenModel.source!!) } }, | ||||
|             onFilterButtonClicked = screenModel::showSettingsDialog, | ||||
|             onRefresh = screenModel::fetchAllFromSource, | ||||
|   | ||||
| @@ -1090,9 +1090,6 @@ class MangaScreenModel( | ||||
|             val filterActive: Boolean | ||||
|                 get() = scanlatorFilterActive || manga.chaptersFiltered() | ||||
|  | ||||
|             val trackingAvailable: Boolean | ||||
|                 get() = trackItems.isNotEmpty() | ||||
|  | ||||
|             val trackingCount: Int | ||||
|                 get() = trackItems.count { it.track != null } | ||||
|  | ||||
|   | ||||
| @@ -53,7 +53,7 @@ object MoreTab : Tab { | ||||
|         } | ||||
|  | ||||
|     override suspend fun onReselect(navigator: Navigator) { | ||||
|         navigator.push(SettingsScreen.toMainScreen()) | ||||
|         navigator.push(SettingsScreen()) | ||||
|     } | ||||
|  | ||||
|     @Composable | ||||
| @@ -72,9 +72,9 @@ object MoreTab : Tab { | ||||
|             onClickDownloadQueue = { navigator.push(DownloadQueueScreen) }, | ||||
|             onClickCategories = { navigator.push(CategoryScreen()) }, | ||||
|             onClickStats = { navigator.push(StatsScreen()) }, | ||||
|             onClickDataAndStorage = { navigator.push(SettingsScreen.toDataAndStorageScreen()) }, | ||||
|             onClickSettings = { navigator.push(SettingsScreen.toMainScreen()) }, | ||||
|             onClickAbout = { navigator.push(SettingsScreen.toAboutScreen()) }, | ||||
|             onClickDataAndStorage = { navigator.push(SettingsScreen(SettingsScreen.Destination.DataAndStorage)) }, | ||||
|             onClickSettings = { navigator.push(SettingsScreen()) }, | ||||
|             onClickAbout = { navigator.push(SettingsScreen(SettingsScreen.Destination.About)) }, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,41 @@ | ||||
| package eu.kanade.tachiyomi.ui.more | ||||
|  | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.remember | ||||
| import cafe.adriel.voyager.navigator.LocalNavigator | ||||
| import cafe.adriel.voyager.navigator.currentOrThrow | ||||
| import eu.kanade.domain.base.BasePreferences | ||||
| import eu.kanade.domain.ui.UiPreferences | ||||
| import eu.kanade.presentation.more.onboarding.OnboardingScreen | ||||
| import eu.kanade.presentation.util.Screen | ||||
| import eu.kanade.tachiyomi.ui.setting.SettingsScreen | ||||
| import tachiyomi.domain.storage.service.StoragePreferences | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| class OnboardingScreen : Screen() { | ||||
|  | ||||
|     @Composable | ||||
|     override fun Content() { | ||||
|         val navigator = LocalNavigator.currentOrThrow | ||||
|  | ||||
|         val basePreferences = remember { Injekt.get<BasePreferences>() } | ||||
|         val storagePreferences = remember { Injekt.get<StoragePreferences>() } | ||||
|         val uiPreferences = remember { Injekt.get<UiPreferences>() } | ||||
|  | ||||
|         val finishOnboarding = { | ||||
|             basePreferences.shownOnboardingFlow().set(true) | ||||
|             navigator.pop() | ||||
|         } | ||||
|  | ||||
|         OnboardingScreen( | ||||
|             storagePreferences = storagePreferences, | ||||
|             uiPreferences = uiPreferences, | ||||
|             onComplete = { finishOnboarding() }, | ||||
|             onRestoreBackup = { | ||||
|                 finishOnboarding() | ||||
|                 navigator.push(SettingsScreen(SettingsScreen.Destination.DataAndStorage)) | ||||
|             }, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -74,6 +74,7 @@ import tachiyomi.domain.source.service.SourceManager | ||||
| import tachiyomi.source.local.isLocal | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.time.Instant | ||||
| import java.util.Date | ||||
|  | ||||
| /** | ||||
| @@ -539,7 +540,7 @@ class ReaderViewModel @JvmOverloads constructor( | ||||
|     } | ||||
|  | ||||
|     fun restartReadTimer() { | ||||
|         chapterReadStartTime = Date().time | ||||
|         chapterReadStartTime = Instant.now().toEpochMilli() | ||||
|     } | ||||
|  | ||||
|     fun flushReadTimer() { | ||||
|   | ||||
| @@ -83,12 +83,6 @@ class SaveImageNotifier(private val context: Context) { | ||||
|                 context.stringResource(MR.strings.action_share), | ||||
|                 NotificationReceiver.shareImagePendingBroadcast(context, uri), | ||||
|             ) | ||||
|             // Delete action | ||||
|             addAction( | ||||
|                 R.drawable.ic_delete_24dp, | ||||
|                 context.stringResource(MR.strings.action_delete), | ||||
|                 NotificationReceiver.deleteImagePendingBroadcast(context, uri), | ||||
|             ) | ||||
|  | ||||
|             updateNotification() | ||||
|         } | ||||
|   | ||||
| @@ -16,6 +16,7 @@ import eu.kanade.presentation.theme.TachiyomiTheme | ||||
| import eu.kanade.tachiyomi.data.download.DownloadManager | ||||
| import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.source.local.isLocal | ||||
|  | ||||
| class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : | ||||
|     AbstractComposeView(context, attrs) { | ||||
| @@ -31,7 +32,7 @@ class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: At | ||||
|             Data( | ||||
|                 transition = transition, | ||||
|                 currChapterDownloaded = transition.from.pageLoader?.isLocal == true, | ||||
|                 goingToChapterDownloaded = transition.to?.chapter?.let { goingToChapter -> | ||||
|                 goingToChapterDownloaded = manga.isLocal() || transition.to?.chapter?.let { goingToChapter -> | ||||
|                     downloadManager.isChapterDownloaded( | ||||
|                         chapterName = goingToChapter.name, | ||||
|                         chapterScanlator = goingToChapter.scanlator, | ||||
|   | ||||
| @@ -15,6 +15,7 @@ import cafe.adriel.voyager.navigator.currentOrThrow | ||||
| import eu.kanade.presentation.more.settings.screen.SettingsAppearanceScreen | ||||
| import eu.kanade.presentation.more.settings.screen.SettingsDataScreen | ||||
| import eu.kanade.presentation.more.settings.screen.SettingsMainScreen | ||||
| import eu.kanade.presentation.more.settings.screen.SettingsTrackingScreen | ||||
| import eu.kanade.presentation.more.settings.screen.about.AboutScreen | ||||
| import eu.kanade.presentation.util.DefaultNavigatorScreenTransition | ||||
| import eu.kanade.presentation.util.LocalBackPress | ||||
| @@ -22,22 +23,22 @@ import eu.kanade.presentation.util.Screen | ||||
| import eu.kanade.presentation.util.isTabletUi | ||||
| import tachiyomi.presentation.core.components.TwoPanelBox | ||||
|  | ||||
| class SettingsScreen private constructor( | ||||
|     val toDataAndStorage: Boolean, | ||||
|     val toAbout: Boolean, | ||||
| class SettingsScreen( | ||||
|     private val destination: Int? = null, | ||||
| ) : Screen() { | ||||
|  | ||||
|     constructor(destination: Destination) : this(destination.id) | ||||
|  | ||||
|     @Composable | ||||
|     override fun Content() { | ||||
|         val parentNavigator = LocalNavigator.currentOrThrow | ||||
|         if (!isTabletUi()) { | ||||
|             Navigator( | ||||
|                 screen = if (toDataAndStorage) { | ||||
|                     SettingsDataScreen | ||||
|                 } else if (toAbout) { | ||||
|                     AboutScreen | ||||
|                 } else { | ||||
|                     SettingsMainScreen | ||||
|                 screen = when (destination) { | ||||
|                     Destination.About.id -> AboutScreen | ||||
|                     Destination.DataAndStorage.id -> SettingsDataScreen | ||||
|                     Destination.Tracking.id -> SettingsTrackingScreen | ||||
|                     else -> SettingsMainScreen | ||||
|                 }, | ||||
|                 content = { | ||||
|                     val pop: () -> Unit = { | ||||
| @@ -54,12 +55,11 @@ class SettingsScreen private constructor( | ||||
|             ) | ||||
|         } else { | ||||
|             Navigator( | ||||
|                 screen = if (toDataAndStorage) { | ||||
|                     SettingsDataScreen | ||||
|                 } else if (toAbout) { | ||||
|                     AboutScreen | ||||
|                 } else { | ||||
|                     SettingsAppearanceScreen | ||||
|                 screen = when (destination) { | ||||
|                     Destination.About.id -> AboutScreen | ||||
|                     Destination.DataAndStorage.id -> SettingsDataScreen | ||||
|                     Destination.Tracking.id -> SettingsTrackingScreen | ||||
|                     else -> SettingsAppearanceScreen | ||||
|                 }, | ||||
|             ) { | ||||
|                 val insets = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal) | ||||
| @@ -78,11 +78,9 @@ class SettingsScreen private constructor( | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     companion object { | ||||
|         fun toMainScreen() = SettingsScreen(toDataAndStorage = false, toAbout = false) | ||||
|  | ||||
|         fun toDataAndStorageScreen() = SettingsScreen(toDataAndStorage = true, toAbout = false) | ||||
|  | ||||
|         fun toAboutScreen() = SettingsScreen(toDataAndStorage = false, toAbout = true) | ||||
|     sealed class Destination(val id: Int) { | ||||
|         data object About : Destination(0) | ||||
|         data object DataAndStorage : Destination(1) | ||||
|         data object Tracking : Destination(2) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -49,7 +49,7 @@ import tachiyomi.domain.updates.interactor.GetUpdates | ||||
| import tachiyomi.domain.updates.model.UpdatesWithRelations | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.util.Calendar | ||||
| import java.time.ZonedDateTime | ||||
| import java.util.Date | ||||
|  | ||||
| class UpdatesScreenModel( | ||||
| @@ -79,13 +79,10 @@ class UpdatesScreenModel( | ||||
|     init { | ||||
|         screenModelScope.launchIO { | ||||
|             // Set date limit for recent chapters | ||||
|             val calendar = Calendar.getInstance().apply { | ||||
|                 time = Date() | ||||
|                 add(Calendar.MONTH, -3) | ||||
|             } | ||||
|             val limit = ZonedDateTime.now().minusMonths(3).toInstant() | ||||
|  | ||||
|             combine( | ||||
|                 getUpdates.subscribe(calendar).distinctUntilChanged(), | ||||
|                 getUpdates.subscribe(limit).distinctUntilChanged(), | ||||
|                 downloadCache.changes, | ||||
|                 downloadManager.queueState, | ||||
|             ) { updates, _, _ -> updates } | ||||
|   | ||||
| @@ -12,7 +12,7 @@ import tachiyomi.source.local.isLocal | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.io.InputStream | ||||
| import java.util.Date | ||||
| import java.time.Instant | ||||
|  | ||||
| /** | ||||
|  * Call before updating [Manga.thumbnail_url] to ensure old cover can be cleared from cache | ||||
| @@ -28,7 +28,7 @@ fun Manga.prepUpdateCover(coverCache: CoverCache, remoteManga: SManga, refreshSa | ||||
|  | ||||
|     return when { | ||||
|         isLocal() -> { | ||||
|             this.copy(coverLastModified = Date().time) | ||||
|             this.copy(coverLastModified = Instant.now().toEpochMilli()) | ||||
|         } | ||||
|         hasCustomCover(coverCache) -> { | ||||
|             coverCache.deleteFromCache(this, false) | ||||
| @@ -36,7 +36,7 @@ fun Manga.prepUpdateCover(coverCache: CoverCache, remoteManga: SManga, refreshSa | ||||
|         } | ||||
|         else -> { | ||||
|             coverCache.deleteFromCache(this, false) | ||||
|             this.copy(coverLastModified = Date().time) | ||||
|             this.copy(coverLastModified = Instant.now().toEpochMilli()) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -44,7 +44,7 @@ fun Manga.prepUpdateCover(coverCache: CoverCache, remoteManga: SManga, refreshSa | ||||
| fun Manga.removeCovers(coverCache: CoverCache = Injekt.get()): Manga { | ||||
|     if (isLocal()) return this | ||||
|     return if (coverCache.deleteFromCache(this, true) > 0) { | ||||
|         return copy(coverLastModified = Date().time) | ||||
|         return copy(coverLastModified = Instant.now().toEpochMilli()) | ||||
|     } else { | ||||
|         this | ||||
|     } | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import java.text.DateFormat | ||||
| import java.time.Instant | ||||
| import java.time.LocalDateTime | ||||
| import java.time.ZoneId | ||||
| import java.time.temporal.ChronoUnit | ||||
| import java.util.Calendar | ||||
| import java.util.Date | ||||
|  | ||||
| @@ -38,13 +39,8 @@ fun Long.convertEpochMillisZone( | ||||
|  * @return date as time key | ||||
|  */ | ||||
| fun Long.toDateKey(): Date { | ||||
|     val cal = Calendar.getInstance() | ||||
|     cal.time = Date(this) | ||||
|     cal[Calendar.HOUR_OF_DAY] = 0 | ||||
|     cal[Calendar.MINUTE] = 0 | ||||
|     cal[Calendar.SECOND] = 0 | ||||
|     cal[Calendar.MILLISECOND] = 0 | ||||
|     return cal.time | ||||
|     val instant = Instant.ofEpochMilli(this) | ||||
|     return Date.from(instant.truncatedTo(ChronoUnit.DAYS)) | ||||
| } | ||||
|  | ||||
| private const val MILLISECONDS_IN_DAY = 86_400_000L | ||||
|   | ||||
| @@ -1,9 +0,0 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:width="24dp" | ||||
|     android:height="24dp" | ||||
|     android:viewportWidth="24" | ||||
|     android:viewportHeight="24"> | ||||
|     <path | ||||
|         android:fillColor="#FFF" | ||||
|         android:pathData="M16,9v10H8V9h8m-1.5,-6h-5l-1,1H5v2h14V4h-3.5l-1,-1zM18,7H6v12c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7z" /> | ||||
| </vector> | ||||
| @@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.stateIn | ||||
|  * Local-copy implementation of PreferenceStore mostly for test and preview purposes | ||||
|  */ | ||||
| class InMemoryPreferenceStore( | ||||
|     private val initialPreferences: Sequence<InMemoryPreference<*>> = sequenceOf(), | ||||
|     initialPreferences: Sequence<InMemoryPreference<*>> = sequenceOf(), | ||||
| ) : PreferenceStore { | ||||
|  | ||||
|     private val preferences: Map<String, Preference<*>> = | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| package tachiyomi.domain.storage.service | ||||
|  | ||||
| import tachiyomi.core.preference.Preference | ||||
| import tachiyomi.core.preference.PreferenceStore | ||||
| import tachiyomi.core.storage.FolderProvider | ||||
|  | ||||
| @@ -8,5 +9,5 @@ class StoragePreferences( | ||||
|     private val preferenceStore: PreferenceStore, | ||||
| ) { | ||||
|  | ||||
|     fun baseStorageDirectory() = preferenceStore.getString("storage_dir", folderProvider.path()) | ||||
|     fun baseStorageDirectory() = preferenceStore.getString(Preference.appStateKey("storage_dir"), folderProvider.path()) | ||||
| } | ||||
|   | ||||
| @@ -3,7 +3,7 @@ package tachiyomi.domain.updates.interactor | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import tachiyomi.domain.updates.model.UpdatesWithRelations | ||||
| import tachiyomi.domain.updates.repository.UpdatesRepository | ||||
| import java.util.Calendar | ||||
| import java.time.Instant | ||||
|  | ||||
| class GetUpdates( | ||||
|     private val repository: UpdatesRepository, | ||||
| @@ -13,8 +13,8 @@ class GetUpdates( | ||||
|         return repository.awaitWithRead(read, after, limit = 500) | ||||
|     } | ||||
|  | ||||
|     fun subscribe(calendar: Calendar): Flow<List<UpdatesWithRelations>> { | ||||
|         return repository.subscribeAll(calendar.time.time, limit = 500) | ||||
|     fun subscribe(instant: Instant): Flow<List<UpdatesWithRelations>> { | ||||
|         return repository.subscribeAll(instant.toEpochMilli(), limit = 500) | ||||
|     } | ||||
|  | ||||
|     fun subscribe(read: Boolean, after: Long): Flow<List<UpdatesWithRelations>> { | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| [versions] | ||||
| compiler = "1.5.5" | ||||
| compiler = "1.5.6" | ||||
| compose-bom = "2023.12.00-alpha03" | ||||
| accompanist = "0.33.2-alpha" | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| [versions] | ||||
| kotlin_version = "1.9.20" | ||||
| serialization_version = "1.6.1" | ||||
| kotlin_version = "1.9.21" | ||||
| serialization_version = "1.6.2" | ||||
| xml_serialization_version = "0.86.2" | ||||
|  | ||||
| [libraries] | ||||
|   | ||||
| @@ -1,13 +1,13 @@ | ||||
| [versions] | ||||
| aboutlib_version = "10.9.2" | ||||
| leakcanary = "2.12" | ||||
| moko = "0.23.0" | ||||
| okhttp_version = "5.0.0-alpha.11" | ||||
| shizuku_version = "12.2.0" | ||||
| sqlite = "2.4.0" | ||||
| sqldelight = "2.0.0" | ||||
| leakcanary = "2.12" | ||||
| voyager = "1.0.0-rc10" | ||||
| richtext = "0.17.0" | ||||
| shizuku_version = "12.2.0" | ||||
| sqldelight = "2.0.0" | ||||
| sqlite = "2.4.0" | ||||
| voyager = "1.0.0" | ||||
|  | ||||
| [libraries] | ||||
| desugar = "com.android.tools:desugar_jdk_libs:2.0.4" | ||||
| @@ -90,6 +90,7 @@ kotest-assertions = "io.kotest:kotest-assertions-core:5.8.0" | ||||
| mockk = "io.mockk:mockk:1.13.8" | ||||
|  | ||||
| voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" } | ||||
| voyager-screenmodel = { module = "cafe.adriel.voyager:voyager-screenmodel", version.ref = "voyager" } | ||||
| voyager-tab-navigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" } | ||||
| voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" } | ||||
|  | ||||
| @@ -104,6 +105,6 @@ sqlite = ["sqlite-framework", "sqlite-ktx", "sqlite-android"] | ||||
| coil = ["coil-core", "coil-gif", "coil-compose"] | ||||
| shizuku = ["shizuku-api", "shizuku-provider"] | ||||
| sqldelight = ["sqldelight-android-driver", "sqldelight-coroutines", "sqldelight-android-paging"] | ||||
| voyager = ["voyager-navigator", "voyager-tab-navigator", "voyager-transitions"] | ||||
| voyager = ["voyager-navigator", "voyager-screenmodel", "voyager-tab-navigator", "voyager-transitions"] | ||||
| richtext = ["richtext-commonmark", "richtext-m3"] | ||||
| test = ["junit", "kotest-assertions", "mockk"] | ||||
							
								
								
									
										5
									
								
								i18n/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								i18n/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| # i18n | ||||
|  | ||||
| This module houses the string resources and translations. | ||||
|  | ||||
| Original English strings are manged in `src/commonMain/resources/MR/base/`. Translations are done externally via Weblate. See [our website](https://tachiyomi.org/docs/contribute#translation) for more details.  | ||||
| @@ -54,7 +54,6 @@ | ||||
|     <string name="theme_dark">በርቷል</string> | ||||
|     <string name="theme_light">ጠፍቷል</string> | ||||
|     <string name="theme_system">ስርዓት ይከተሉ</string> | ||||
|     <string name="pref_theme_mode">ጨለማ ሁነታ</string> | ||||
|     <string name="pref_category_theme">ገጽታ</string> | ||||
|     <string name="pref_category_about">ስለ</string> | ||||
|     <string name="pref_category_advanced">የላቀ</string> | ||||
|   | ||||
| @@ -309,7 +309,6 @@ | ||||
|     <string name="theme_system">اتبع مظهر النظام</string> | ||||
|     <string name="theme_dark">مفعّل</string> | ||||
|     <string name="theme_light">غير مفعّل</string> | ||||
|     <string name="pref_theme_mode">الوضع الليلي</string> | ||||
|     <string name="battery_optimization_disabled">تم إلغاء وضع تحسين البطارية مُسبقاً</string> | ||||
|     <string name="pref_disable_battery_optimization_summary">يساعد في عملية تحديث المكتبة والنسخ الإحتياطي في الخلفية</string> | ||||
|     <string name="pref_disable_battery_optimization">إطفاء وضع تحسين البطارية</string> | ||||
|   | ||||
| @@ -174,6 +174,19 @@ | ||||
|     <!-- Shortcuts--> | ||||
|     <string name="app_not_available">App not available</string> | ||||
|  | ||||
|     <!-- Onboarding --> | ||||
|     <string name="pref_onboarding_guide">Onboarding guide</string> | ||||
|     <string name="onboarding_heading">Welcome!</string> | ||||
|     <string name="onboarding_description">Let\'s set some things up first. You can always change these in the settings later too.</string> | ||||
|     <string name="onboarding_action_next">Next</string> | ||||
|     <string name="onboarding_action_finish">Get started</string> | ||||
|     <string name="onboarding_action_skip">Skip</string> | ||||
|     <string name="onboarding_storage_info">Select a folder where %1$s will store chapter downloads, backups, and more.\n\nA dedicated folder is recommended.\n\nSelected folder: %2$s</string> | ||||
|     <string name="onboarding_storage_action_select">Select a folder</string> | ||||
|     <string name="onboarding_storage_selection_required">A folder must be selected</string> | ||||
|     <string name="onboarding_guides_new_user">New to %s? We recommend checking out the getting started guide.</string> | ||||
|     <string name="onboarding_guides_returning_user">Already used %s before?</string> | ||||
|  | ||||
|     <!-- Preferences --> | ||||
|       <!-- Subsections --> | ||||
|     <string name="pref_category_general">General</string> | ||||
| @@ -198,11 +211,10 @@ | ||||
|  | ||||
|       <!-- General section --> | ||||
|     <string name="pref_category_theme">Theme</string> | ||||
|     <string name="pref_theme_mode">Dark mode</string> | ||||
|     <string name="theme_system">Follow system</string> | ||||
|     <string name="theme_light">Off</string> | ||||
|     <string name="theme_dark">On</string> | ||||
|     <string name="pref_app_theme">App theme</string> | ||||
|     <string name="theme_system">System</string> | ||||
|     <string name="theme_light">Light</string> | ||||
|     <string name="theme_dark">Dark</string> | ||||
|     <string name="theme_monet">Dynamic</string> | ||||
|     <string name="theme_greenapple">Green Apple</string> | ||||
|     <string name="theme_lavender">Lavender</string> | ||||
| @@ -436,6 +448,7 @@ | ||||
|     <string name="pref_remove_after_read">After reading automatically delete</string> | ||||
|     <string name="pref_remove_bookmarked_chapters">Allow deleting bookmarked chapters</string> | ||||
|     <string name="pref_remove_exclude_categories">Excluded categories</string> | ||||
|     <string name="no_location_set">No storage location set</string> | ||||
|     <string name="invalid_location">Invalid location: %s</string> | ||||
|     <string name="disabled">Disabled</string> | ||||
|     <string name="last_read_chapter">Last read chapter</string> | ||||
|   | ||||
| @@ -150,7 +150,6 @@ | ||||
|     <string name="theme_dark">Уключаны</string> | ||||
|     <string name="theme_light">Выключаны</string> | ||||
|     <string name="theme_system">У адпаведнасці з сістэмнай тэмай</string> | ||||
|     <string name="pref_theme_mode">Цёмны рэжым</string> | ||||
|     <string name="pref_category_theme">Тэма</string> | ||||
|     <string name="pref_category_about">Інфармацыя</string> | ||||
|     <string name="pref_category_advanced">Дадаткова</string> | ||||
|   | ||||
| @@ -350,7 +350,6 @@ | ||||
|     <string name="theme_system">Система на абонаментите</string> | ||||
|     <string name="theme_dark">Включено</string> | ||||
|     <string name="theme_light">Изключено</string> | ||||
|     <string name="pref_theme_mode">Тъмен режим</string> | ||||
|     <string name="action_move_to_top">Премести най-горе</string> | ||||
|     <string name="action_move_to_bottom">Премести най-долу</string> | ||||
|     <string name="action_oldest">Най-стари</string> | ||||
|   | ||||
| @@ -315,7 +315,6 @@ | ||||
|     <string name="theme_dark">চালু করুন</string> | ||||
|     <string name="theme_light">বন্ধ করুন</string> | ||||
|     <string name="theme_system">সিস্টেমকে অনুসরণ করুন</string> | ||||
|     <string name="pref_theme_mode">অন্ধকার মোড</string> | ||||
|     <string name="pref_category_theme">থিম</string> | ||||
|     <string name="action_move_to_bottom">নীচে সরান</string> | ||||
|     <string name="action_move_to_top">শীর্ষে সরান</string> | ||||
|   | ||||
| @@ -301,7 +301,6 @@ | ||||
|     <string name="action_sort_latest_chapter">Darrer capítol</string> | ||||
|     <string name="action_view_chapters">Mostra els capítols</string> | ||||
|     <string name="action_cancel_all">Cancel·la-ho tot</string> | ||||
|     <string name="pref_theme_mode">Mode fosc</string> | ||||
|     <string name="theme_light">Desactivat</string> | ||||
|     <string name="theme_dark">Activat</string> | ||||
|     <string name="theme_system">Per defecte del sistema</string> | ||||
|   | ||||
| @@ -115,7 +115,6 @@ | ||||
|     <string name="pref_category_tracking">Pagsubay</string> | ||||
|     <string name="pref_category_advanced">Abante</string> | ||||
|     <string name="pref_category_theme">Tema</string> | ||||
|     <string name="pref_theme_mode">Dark mode</string> | ||||
|     <string name="theme_dark">Sa</string> | ||||
|     <string name="pref_app_theme">Tema sa app</string> | ||||
|     <string name="theme_monet">Dinamiko</string> | ||||
|   | ||||
| @@ -392,7 +392,6 @@ | ||||
|     <string name="lock_with_biometrics">Vyžadovat odemknutí</string> | ||||
|     <string name="theme_dark">Zapnuto</string> | ||||
|     <string name="theme_light">Vypnuto</string> | ||||
|     <string name="pref_theme_mode">Temný vzhled</string> | ||||
|     <string name="pref_category_theme">Vzhled</string> | ||||
|     <string name="action_move_to_top">Přesunout nahoru</string> | ||||
|     <string name="action_desc">Sestupně</string> | ||||
|   | ||||
| @@ -155,7 +155,6 @@ | ||||
|     <string name="action_open_log">Тӑвӑм-пулӑм кӗнекине уҫ</string> | ||||
|     <string name="action_reset">Тасат</string> | ||||
|     <string name="action_sort">Уйӑр</string> | ||||
|     <string name="pref_theme_mode">Тӗксӗм темӑ</string> | ||||
|     <string name="theme_system">Системри пекех</string> | ||||
|     <string name="pref_category_reader">Вулӑш</string> | ||||
|     <string name="pref_category_tracking">Йӗрлев</string> | ||||
|   | ||||
| @@ -126,7 +126,6 @@ | ||||
|     <string name="pref_category_advanced">Avanceret</string> | ||||
|     <string name="pref_category_about">Om</string> | ||||
|     <string name="pref_category_theme">Tema</string> | ||||
|     <string name="pref_theme_mode">Mørk tilstand</string> | ||||
|     <string name="theme_system">Følg system</string> | ||||
|     <string name="theme_light">Fra</string> | ||||
|     <string name="theme_dark">Til</string> | ||||
|   | ||||
| @@ -301,7 +301,6 @@ | ||||
|     <string name="action_sort_latest_chapter">Neuestes Kapitel</string> | ||||
|     <string name="action_view_chapters">Kapitel anzeigen</string> | ||||
|     <string name="action_cancel_all">Alle abbrechen</string> | ||||
|     <string name="pref_theme_mode">Dunkelmodus</string> | ||||
|     <string name="theme_light">Aus</string> | ||||
|     <string name="theme_dark">An</string> | ||||
|     <string name="theme_system">Systemeinstellung</string> | ||||
|   | ||||
| @@ -350,7 +350,6 @@ | ||||
|     <string name="theme_system">Ακολουθήστε το σύστημα</string> | ||||
|     <string name="theme_dark">Ενεργοποιημένο</string> | ||||
|     <string name="theme_light">Απενεργοποιημένο</string> | ||||
|     <string name="pref_theme_mode">Σκοτεινή λειτουργία</string> | ||||
|     <string name="action_move_to_bottom">Μετακίνηση στον πάτο</string> | ||||
|     <string name="action_move_to_top">Μετακίνηση στην κορυφή</string> | ||||
|     <string name="action_cancel_all">Ακύρωση όλων</string> | ||||
|   | ||||
| @@ -62,7 +62,6 @@ | ||||
|     <string name="theme_dark">Ŝalti</string> | ||||
|     <string name="theme_light">Malŝalti</string> | ||||
|     <string name="theme_system">Laŭ operaciumo</string> | ||||
|     <string name="pref_theme_mode">Malhela etoso</string> | ||||
|     <string name="pref_category_theme">Etoso</string> | ||||
|     <string name="pref_category_about">Pri</string> | ||||
|     <string name="pref_category_downloads">Elŝutoj</string> | ||||
|   | ||||
| @@ -302,7 +302,6 @@ | ||||
|     <string name="action_sort_latest_chapter">Por capítulo más reciente</string> | ||||
|     <string name="action_view_chapters">Ver capítulos</string> | ||||
|     <string name="action_cancel_all">Cancelar todo</string> | ||||
|     <string name="pref_theme_mode">Modo oscuro</string> | ||||
|     <string name="theme_light">No</string> | ||||
|     <string name="theme_dark">Sí</string> | ||||
|     <string name="theme_system">Según ajustes del sistema</string> | ||||
|   | ||||
| @@ -492,7 +492,6 @@ | ||||
|     <string name="pref_category_downloads">Deskargak</string> | ||||
|     <string name="pref_category_tracking">Jarraipena</string> | ||||
|     <string name="pref_category_advanced">Aurreratua</string> | ||||
|     <string name="pref_theme_mode">Modu iluna</string> | ||||
|     <string name="action_display_language_badge">Hizkuntza</string> | ||||
|     <string name="theme_strawberrydaiquiri">Marrubi Daiquiri-a</string> | ||||
|     <string name="theme_tako">Tako</string> | ||||
|   | ||||
| @@ -261,7 +261,6 @@ | ||||
|     <string name="theme_dark">روشن</string> | ||||
|     <string name="theme_light">خاموش</string> | ||||
|     <string name="theme_system">تم پیشفرض سیستم</string> | ||||
|     <string name="pref_theme_mode">تم تیره</string> | ||||
|     <string name="pref_category_about">درباره</string> | ||||
|     <string name="pref_category_advanced">پیشرفته</string> | ||||
|     <string name="pref_category_tracking">ردیابی</string> | ||||
| @@ -614,4 +613,19 @@ | ||||
|     <string name="action_ok">باشه</string> | ||||
|     <string name="action_sort_next_updated">به روز رسانی مورد انتظار بعدی</string> | ||||
|     <string name="download_queue_size_warning">هشدار: حجم زیاد بارگیری ممکن است باعث اهسته تر شدن سرعت ویا مسدود کردن Tachiyomi از منبع شود. برای اطلاعات بیشتر لمس کنید.</string> | ||||
|     <string name="skipped_reason_not_always_update">به دلیل این که این مجموعه نیازی به به روز رسانی نداشت رد شد</string> | ||||
|     <string name="skipped_reason_not_caught_up">به دلیل وجود چپتر های خوانده نشده رد شد</string> | ||||
|     <string name="notification_update_skipped">%1$d بزور رسانی رد شد</string> | ||||
|     <string name="skipped_reason_not_started">به دلیل این که هیچ چپتری خوانده نشده بود رد شد</string> | ||||
|     <string name="notification_update_error">%1$d بروز رسانی ناموفق</string> | ||||
|     <string name="pref_relative_format_summary">\"%1$s\" به جای \"%2$s\"</string> | ||||
|     <string name="pref_library_columns_per_row">%d در هر ردیف</string> | ||||
|     <string name="pref_page_rotate">صفحات عریض را بچرخان تا جا شوند</string> | ||||
|     <string name="popular">محبوب</string> | ||||
|     <string name="pref_chapter_swipe_end">حرکت کشیدن به راست</string> | ||||
|     <string name="pref_flash_page">در هنگام عوض شدن صفحه فلاش سفید بزن</string> | ||||
|     <string name="pref_chapter_swipe_start">حرکت کشیدن به چپ</string> | ||||
|     <string name="updates_last_update_info">آخرین به روز رسانی کتابخانه: %s</string> | ||||
|     <string name="pref_update_only_in_release_period">خارج از دوره انتشار موزد انتظار</string> | ||||
|     <string name="pref_double_tap_zoom">برای بزرگ نمایی دوبار ضربه بزنید</string> | ||||
| </resources> | ||||
| @@ -301,7 +301,6 @@ | ||||
|     <string name="action_sort_latest_chapter">Viimeisin luku</string> | ||||
|     <string name="action_view_chapters">Näytä luvut</string> | ||||
|     <string name="action_cancel_all">Peruuta kaikki</string> | ||||
|     <string name="pref_theme_mode">Pimeä tila</string> | ||||
|     <string name="theme_light">Pois päältä</string> | ||||
|     <string name="theme_dark">Päällä</string> | ||||
|     <string name="theme_system">Seuraa järjestelmää</string> | ||||
|   | ||||
| @@ -102,7 +102,6 @@ | ||||
|     <string name="theme_dark">Nakabukas</string> | ||||
|     <string name="theme_light">Nakasara</string> | ||||
|     <string name="theme_system">Sundan ang sistema</string> | ||||
|     <string name="pref_theme_mode">Madilim na tema</string> | ||||
|     <string name="pref_category_about">Patungkol</string> | ||||
|     <string name="pref_category_advanced">Karagdagan</string> | ||||
|     <string name="pref_category_tracking">Pagta-track</string> | ||||
| @@ -272,7 +271,7 @@ | ||||
|     <string name="notification_chapters_single_and_more">Kabanata %1$s at karagdagang %2$d pa</string> | ||||
|     <string name="notification_chapters_single">Kabanata %1$s</string> | ||||
|     <string name="notification_new_chapters">May mga bagong kabanata</string> | ||||
|     <string name="download_insufficient_space">Di ma-download ang mga kabanata dahil sa mababang espasyo</string> | ||||
|     <string name="download_insufficient_space">Di ma-download ang mga kabanata dahil sa mababang espasyo sa storage</string> | ||||
|     <string name="download_queue_error">Di ma-download ang mga kabanata. Subukan mo uli ito sa Dina-download</string> | ||||
|     <string name="copy">Kopyahin</string> | ||||
|     <string name="migrate">Ilipat</string> | ||||
| @@ -644,7 +643,7 @@ | ||||
|     <string name="pref_downloads_summary">Kusang pag-download, i-download agad</string> | ||||
|     <string name="pref_tracking_summary">Isahang pagsabay sa progress, pinahusay na pagsabay</string> | ||||
|     <string name="pref_appearance_summary">Tema, ayos ng petsa & oras</string> | ||||
|     <string name="pref_backup_summary">Mano-mano at kusang pag-backup</string> | ||||
|     <string name="pref_backup_summary">Mano-mano at awtomatikong pag-backup, espasyo sa storage</string> | ||||
|     <string name="pref_security_summary">Pag-lock aa app, bantayan ang screen</string> | ||||
|     <string name="pref_advanced_summary">Itambak ang mga crash log, pag-o-optimisa sa baterya</string> | ||||
|     <string name="pref_library_summary">Mga kategorya, panlahatang update, pag-swipe ng kabanata</string> | ||||
| @@ -764,7 +763,7 @@ | ||||
|     <string name="exclude_scanlators">Ibukod ang mga scanlator</string> | ||||
|     <string name="action_create">Lumikha</string> | ||||
|     <string name="pref_storage_location">Lokasyon ng storage</string> | ||||
|     <string name="pref_storage_location_info">Ginagamit para sa automatikong pa-backup, pag-download ng mga kabanata, at lokal na source.</string> | ||||
|     <string name="pref_storage_location_info">Ginagamit para sa automatikong pag-backup, pag-download ng mga kabanata, at lokal na source.</string> | ||||
|     <string name="action_menu_overflow_description">Ibang opsiyon</string> | ||||
|     <string name="selected">Napili</string> | ||||
|     <string name="not_selected">Di napili</string> | ||||
|   | ||||
| @@ -301,7 +301,6 @@ | ||||
|     <string name="action_sort_latest_chapter">Dernier chapitre</string> | ||||
|     <string name="action_view_chapters">Voir les chapitres</string> | ||||
|     <string name="action_cancel_all">Tout annuler</string> | ||||
|     <string name="pref_theme_mode">Mode sombre</string> | ||||
|     <string name="theme_light">Désactivé</string> | ||||
|     <string name="theme_dark">Activé</string> | ||||
|     <string name="theme_system">Par défaut du système</string> | ||||
|   | ||||
| @@ -138,7 +138,6 @@ | ||||
|     <string name="theme_dark">Activado</string> | ||||
|     <string name="theme_light">Desactivado</string> | ||||
|     <string name="theme_system">Utilizar o do sistema</string> | ||||
|     <string name="pref_theme_mode">Modo escuro</string> | ||||
|     <string name="pref_category_theme">Tema</string> | ||||
|     <string name="pref_category_about">Acerca de</string> | ||||
|     <string name="pref_category_advanced">Avanzado</string> | ||||
|   | ||||
| @@ -141,7 +141,6 @@ | ||||
|     <string name="pref_date_format">תבנית תאריך</string> | ||||
|     <string name="theme_dark">פעיל</string> | ||||
|     <string name="theme_light">כבוי</string> | ||||
|     <string name="pref_theme_mode">מצב חשוך</string> | ||||
|     <string name="pref_category_about">אודות</string> | ||||
|     <string name="pref_category_advanced">מתקדם</string> | ||||
|     <string name="pref_category_downloads">הורדות</string> | ||||
|   | ||||
| @@ -301,7 +301,6 @@ | ||||
|     <string name="action_sort_latest_chapter">नवीनतम अध्याय</string> | ||||
|     <string name="action_view_chapters">अध्याय देखें</string> | ||||
|     <string name="action_cancel_all">सब रद्द करो</string> | ||||
|     <string name="pref_theme_mode">डार्क मोड</string> | ||||
|     <string name="theme_light">बंद</string> | ||||
|     <string name="theme_dark">चालू करे</string> | ||||
|     <string name="theme_system">सिस्टम का पालन करें</string> | ||||
|   | ||||
| @@ -50,7 +50,6 @@ | ||||
|     <string name="theme_system">Slijedi sustav</string> | ||||
|     <string name="theme_dark">Uključeno</string> | ||||
|     <string name="theme_light">Isključeno</string> | ||||
|     <string name="pref_theme_mode">Tamna tema</string> | ||||
|     <string name="pref_category_about">Informacije</string> | ||||
|     <string name="pref_category_advanced">Napredno</string> | ||||
|     <string name="pref_category_tracking">Praćenje</string> | ||||
|   | ||||
| @@ -176,7 +176,6 @@ | ||||
|     <string name="pref_category_security">Biztonság</string> | ||||
|     <string name="pref_manage_notifications">Értesítések kezelése</string> | ||||
|     <string name="theme_system">Rendszerbeállítás követése</string> | ||||
|     <string name="pref_theme_mode">Sötét mód</string> | ||||
|     <string name="pref_category_theme">Téma</string> | ||||
|     <string name="action_move_to_bottom">Ugrás legalulra</string> | ||||
|     <string name="action_move_to_top">Ugrás legfelülre</string> | ||||
|   | ||||
| @@ -293,7 +293,6 @@ | ||||
|     <string name="action_sort_latest_chapter">Bab terbaru</string> | ||||
|     <string name="action_view_chapters">Lihat bab</string> | ||||
|     <string name="action_cancel_all">Batalkan semua</string> | ||||
|     <string name="pref_theme_mode">Mode gelap</string> | ||||
|     <string name="theme_light">Mati</string> | ||||
|     <string name="theme_dark">Nyala</string> | ||||
|     <string name="theme_system">Ikuti sistem</string> | ||||
|   | ||||
| @@ -301,7 +301,6 @@ | ||||
|     <string name="action_sort_latest_chapter">Ultimo capitolo</string> | ||||
|     <string name="action_view_chapters">Visualizza capitoli</string> | ||||
|     <string name="action_cancel_all">Annulla tutto</string> | ||||
|     <string name="pref_theme_mode">Tema scuro</string> | ||||
|     <string name="theme_light">Disattivo</string> | ||||
|     <string name="theme_dark">Attivo</string> | ||||
|     <string name="theme_system">Usa il tema di sistema</string> | ||||
|   | ||||
| @@ -296,7 +296,6 @@ | ||||
|     <string name="action_sort_latest_chapter">最新章の更新順</string> | ||||
|     <string name="action_view_chapters">章を見る</string> | ||||
|     <string name="action_cancel_all">すべてキャンセル</string> | ||||
|     <string name="pref_theme_mode">ダークモード</string> | ||||
|     <string name="theme_light">オフ</string> | ||||
|     <string name="theme_dark">オン</string> | ||||
|     <string name="theme_system">システムに従う</string> | ||||
| @@ -726,8 +725,8 @@ | ||||
|     <string name="track_delete_text">ローカルの追跡が削除されます。</string> | ||||
|     <string name="track_delete_remote_text">%s からも削除</string> | ||||
|     <string name="delete_downloaded">ダウンロードを削除</string> | ||||
|     <string name="action_filter_interval_late">10+チェック後半</string> | ||||
|     <string name="action_filter_interval_dropped">落とした? 20歳以上後半と2ヶ月</string> | ||||
|     <string name="action_filter_interval_late">レイト10+チェック</string> | ||||
|     <string name="action_filter_interval_dropped">落選? 20歳後半と2ヶ月</string> | ||||
|     <string name="action_filter_interval_passed">チェック期間を過ぎました</string> | ||||
|     <string name="action_ok">OK</string> | ||||
|     <string name="syncing_library">ライブラリを同期しています</string> | ||||
|   | ||||
| @@ -42,7 +42,6 @@ | ||||
|     <string name="pref_date_format">Format tanggal</string> | ||||
|     <string name="theme_dark">Murup</string> | ||||
|     <string name="theme_light">Mati</string> | ||||
|     <string name="pref_theme_mode">Mode Peteng</string> | ||||
|     <string name="pref_category_theme">Tema</string> | ||||
|     <string name="pref_category_downloads">Donlot</string> | ||||
|     <string name="action_webview_back">Bali</string> | ||||
|   | ||||
| @@ -79,7 +79,6 @@ | ||||
|     <string name="pref_category_tracking">თვალყურის დევნება</string> | ||||
|     <string name="pref_category_advanced">დამატებით</string> | ||||
|     <string name="pref_category_about">ინფორმაცია</string> | ||||
|     <string name="pref_theme_mode">ბნელი რეჟიმი</string> | ||||
|     <string name="theme_system">სისტემური</string> | ||||
|     <string name="theme_light">მსუბუქი</string> | ||||
|     <string name="theme_dark">ჩართული</string> | ||||
|   | ||||
| @@ -57,7 +57,6 @@ | ||||
|     <string name="pref_category_downloads">Жүктеулер</string> | ||||
|     <string name="pref_category_advanced">Толығырақ</string> | ||||
|     <string name="pref_category_theme">Кейіп</string> | ||||
|     <string name="pref_theme_mode">Қараңғы режим</string> | ||||
|     <string name="theme_system">Жүйе бойынша</string> | ||||
|     <string name="theme_light">Өшірулі</string> | ||||
|     <string name="theme_dark">Қосулы</string> | ||||
|   | ||||
| @@ -120,7 +120,6 @@ | ||||
|     <string name="pref_category_advanced">ការកំណត់ពិសេស</string> | ||||
|     <string name="pref_category_about">អំពីរ</string> | ||||
|     <string name="pref_category_theme">ពណ៌</string> | ||||
|     <string name="pref_theme_mode">ខ្មៅ</string> | ||||
|     <string name="theme_system">តាមទូរសព្ទ</string> | ||||
|     <string name="theme_light">បិទ</string> | ||||
|     <string name="theme_dark">បើក</string> | ||||
|   | ||||
| @@ -17,7 +17,6 @@ | ||||
|     <string name="theme_system">ಸಿಸ್ಟಮ್ ಅನುಕರಿಸಿ</string> | ||||
|     <string name="theme_dark">ಆನ್</string> | ||||
|     <string name="theme_light">ಆಫ</string> | ||||
|     <string name="pref_theme_mode">ಡಾರ್ಕ್ ಮೋಡ್</string> | ||||
|     <string name="pref_category_about">ಅಪ್ಲಿಕೇಶನ್ ಬಗ್ಗೆ</string> | ||||
|     <string name="pref_category_advanced">ಸುಧಾರಿತ</string> | ||||
|     <string name="pref_category_tracking">ಟ್ರ್ಯಾಕಿಂಗ್</string> | ||||
|   | ||||
| @@ -342,7 +342,6 @@ | ||||
|     <string name="theme_dark">켜기</string> | ||||
|     <string name="theme_light">끄기</string> | ||||
|     <string name="theme_system">시스템 설정 사용</string> | ||||
|     <string name="pref_theme_mode">다크 모드</string> | ||||
|     <string name="action_disable_all">모두 비활성화</string> | ||||
|     <string name="spen_next_page">다음 페이지</string> | ||||
|     <string name="confirm_lock_change">변경 확인을 위해 인증이 필요합니다</string> | ||||
|   | ||||
| @@ -73,7 +73,6 @@ | ||||
|     <string name="theme_dark">Įjungta</string> | ||||
|     <string name="theme_light">Išjungta</string> | ||||
|     <string name="theme_system">Pagal sistemą</string> | ||||
|     <string name="pref_theme_mode">Tamsi</string> | ||||
|     <string name="pref_category_theme">Tema</string> | ||||
|     <string name="pref_category_about">Apie</string> | ||||
|     <string name="pref_category_advanced">Papildomi</string> | ||||
|   | ||||
| @@ -139,7 +139,6 @@ | ||||
|     <string name="theme_dark">Ieslēgts</string> | ||||
|     <string name="theme_light">Izslēgts</string> | ||||
|     <string name="theme_system">Sekot sistēmu</string> | ||||
|     <string name="pref_theme_mode">Tumšais režīms</string> | ||||
|     <string name="pref_category_theme">Motīvs</string> | ||||
|     <string name="pref_category_library">Bibliotēka</string> | ||||
|     <string name="action_webview_refresh">Atjaunot</string> | ||||
| @@ -744,4 +743,29 @@ | ||||
|     <string name="action_sort_category">Kārtot kategorijas</string> | ||||
|     <string name="action_sort_tracker_score">Izsekošanas rezultāts</string> | ||||
|     <string name="label_data_storage">Dati un uzglabāšana</string> | ||||
|     <string name="pref_storage_location">Krātuves atrašanās vieta</string> | ||||
|     <string name="action_create">Izveidot</string> | ||||
|     <string name="relative_time_span_never">Nekad</string> | ||||
|     <string name="pref_flash_page_summ">Samazina spoku rašanos uz e-ink displejiem</string> | ||||
|     <string name="pref_storage_location_info">Izmanto automātiskajām dublējumkopijām, nodaļu lejupielādei un vietējam avotam.</string> | ||||
|     <string name="action_apply">Pieteikties</string> | ||||
|     <string name="action_revert_to_default">Atgriezt noklusējuma iestatījumus</string> | ||||
|     <string name="action_menu_overflow_description">Vairāk iespēju</string> | ||||
|     <string name="last_auto_backup_info">Pēdējā automātiskā dublēšana: %s</string> | ||||
|     <string name="selected">Atlasīts</string> | ||||
|     <string name="no_scanlators_found">Nav atrasts neviens scanlators</string> | ||||
|     <string name="not_selected">Nav atlasīts</string> | ||||
|     <string name="action_move_to_bottom_all_for_series">Pārvietot sēriju uz apakšu</string> | ||||
|     <string name="scanlator">Skanlators</string> | ||||
|     <string name="pref_flash_page">Zibsnīt baltu, kad maina lapu</string> | ||||
|     <string name="pref_storage_usage">Krātuves izmantošana</string> | ||||
|     <string name="notification_updating_progress">Bibliotēkas atjaunināšana... (%s)</string> | ||||
|     <string name="action_bar_up_description">Virzīties uz augšu</string> | ||||
|     <string name="sort_category_confirmation">Vai vēlaties kategorijas sakārtot pēc alfabēta?</string> | ||||
|     <string name="file_null_uri_error">Nav atlasīts neviens fails</string> | ||||
|     <string name="source_settings">Avota iestatījumi</string> | ||||
|     <string name="app_settings">Lietotnes iestatījumi</string> | ||||
|     <string name="pref_relative_format">Relatīviās laika stampas</string> | ||||
|     <string name="pref_relative_format_summary">\"%1$s\", nevis \"%2$s\"</string> | ||||
|     <string name="exclude_scanlators">Izslēgt skanlatorus</string> | ||||
| </resources> | ||||
| @@ -97,7 +97,6 @@ | ||||
|     <string name="theme_system">तंत्राचे अनुसरण करा</string> | ||||
|     <string name="theme_dark">डार्क</string> | ||||
|     <string name="theme_light">लाइट</string> | ||||
|     <string name="pref_theme_mode">गडद मोड</string> | ||||
|     <string name="pref_category_about">ॲप बद्दल</string> | ||||
|     <string name="pref_category_advanced">प्रगत</string> | ||||
|     <string name="pref_category_tracking">ट्रॅकिंग</string> | ||||
|   | ||||
| @@ -301,7 +301,6 @@ | ||||
|     <string name="action_sort_latest_chapter">Bab terkini</string> | ||||
|     <string name="action_view_chapters">Buka bab</string> | ||||
|     <string name="action_cancel_all">Batalkan semua</string> | ||||
|     <string name="pref_theme_mode">Tema gelap</string> | ||||
|     <string name="theme_light">Mati</string> | ||||
|     <string name="theme_dark">Hidup</string> | ||||
|     <string name="theme_system">Ikut sistem</string> | ||||
|   | ||||
| @@ -300,7 +300,6 @@ | ||||
|     <string name="label_more">Mer</string> | ||||
|     <string name="action_view_chapters">Vis kapitler</string> | ||||
|     <string name="action_cancel_all">Avbryt alle</string> | ||||
|     <string name="pref_theme_mode">Mørk</string> | ||||
|     <string name="theme_light">Av</string> | ||||
|     <string name="theme_dark">På</string> | ||||
|     <string name="theme_system">System</string> | ||||
|   | ||||
| @@ -2,7 +2,6 @@ | ||||
| <resources> | ||||
|     <string name="theme_dark">अन</string> | ||||
|     <string name="theme_light">अफ</string> | ||||
|     <string name="pref_theme_mode">अँध्यारो मोड</string> | ||||
|     <string name="pref_category_theme">थीम</string> | ||||
|     <string name="pref_category_about">बारेमा</string> | ||||
|     <string name="pref_category_advanced">उन्नत सेटिङहरू</string> | ||||
|   | ||||
| @@ -301,7 +301,6 @@ | ||||
|     <string name="action_sort_latest_chapter">Laatste hoofdstuk</string> | ||||
|     <string name="action_view_chapters">Hoofdstukken bekijken</string> | ||||
|     <string name="action_cancel_all">Alles annuleren</string> | ||||
|     <string name="pref_theme_mode">Donkere modus</string> | ||||
|     <string name="theme_light">Uit</string> | ||||
|     <string name="theme_dark">Aan</string> | ||||
|     <string name="theme_system">Volg systeeminstelling</string> | ||||
|   | ||||
| @@ -316,7 +316,6 @@ | ||||
|     <string name="theme_system">Systemowy</string> | ||||
|     <string name="theme_dark">Włącz</string> | ||||
|     <string name="theme_light">Wyłącz</string> | ||||
|     <string name="pref_theme_mode">Ciemny motyw</string> | ||||
|     <string name="action_move_to_bottom">Przenieś na dół</string> | ||||
|     <string name="action_move_to_top">Przenieś na górę</string> | ||||
|     <string name="action_oldest">Najstarsze</string> | ||||
|   | ||||
| @@ -301,10 +301,9 @@ | ||||
|     <string name="action_sort_latest_chapter">Último capítulo</string> | ||||
|     <string name="action_view_chapters">Visualizar os capítulos</string> | ||||
|     <string name="action_cancel_all">Cancelar todos</string> | ||||
|     <string name="pref_theme_mode">Modo noturno</string> | ||||
|     <string name="theme_light">Desligado</string> | ||||
|     <string name="theme_dark">Ligado</string> | ||||
|     <string name="theme_system">Seguir o sistema</string> | ||||
|     <string name="theme_light">Claro</string> | ||||
|     <string name="theme_dark">Escuro</string> | ||||
|     <string name="theme_system">Sistema</string> | ||||
|     <string name="pref_manage_notifications">Gerenciar notificações</string> | ||||
|     <string name="pref_category_security">Segurança e privacidade</string> | ||||
|     <string name="lock_with_biometrics">Exigir desbloqueio</string> | ||||
| @@ -769,4 +768,19 @@ | ||||
|     <string name="pref_storage_location_info">Usado para backups automáticos, downloads de capítulos e na fonte local.</string> | ||||
|     <string name="action_menu_overflow_description">Mais opções</string> | ||||
|     <string name="action_bar_up_description">Navegar para cima</string> | ||||
|     <string name="onboarding_storage_action_select">Selecionar uma pasta</string> | ||||
|     <string name="pref_onboarding_guide">Guia de introdução</string> | ||||
|     <string name="onboarding_guides_new_user">Novo no %s? Recomendamos dar uma olhada no guia de introdução.</string> | ||||
|     <string name="onboarding_action_finish">Começar</string> | ||||
|     <string name="onboarding_heading">Bem-vindo(a)!</string> | ||||
|     <string name="onboarding_guides_returning_user">Já utilizou o %s antes?</string> | ||||
|     <string name="onboarding_action_skip">Pular</string> | ||||
|     <string name="onboarding_action_next">Próximo</string> | ||||
|     <string name="onboarding_description">Vamos definir algumas coisas primeiro. Você sempre pode fazer alterações nas configurações depois também.</string> | ||||
|     <string name="no_location_set">Local de armazenamento não definido</string> | ||||
|     <string name="onboarding_storage_info">Escolha uma pasta onde o %1$s irá armazenar os downloads de capítulos, backups e mais. | ||||
| \n | ||||
| \nUma pasta dedicada é recomendada. | ||||
| \n | ||||
| \nPasta selecionada: %2$s</string> | ||||
| </resources> | ||||
| @@ -301,7 +301,6 @@ | ||||
|     <string name="action_sort_latest_chapter">Último capítulo</string> | ||||
|     <string name="action_view_chapters">Ver capítulos</string> | ||||
|     <string name="action_cancel_all">Cancelar tudo</string> | ||||
|     <string name="pref_theme_mode">Modo escuro</string> | ||||
|     <string name="theme_light">Desligado</string> | ||||
|     <string name="theme_dark">Ligado</string> | ||||
|     <string name="theme_system">Seguir o do sistema</string> | ||||
|   | ||||
| @@ -65,4 +65,19 @@ | ||||
|         <item quantity="few">Următoarele %d capitole necitite</item> | ||||
|         <item quantity="other">Următoarele %d capitole necitite</item> | ||||
|     </plurals> | ||||
|     <plurals name="download_amount"> | ||||
|         <item quantity="one">Următorul capitol</item> | ||||
|         <item quantity="few">Următoarele %d capitole</item> | ||||
|         <item quantity="other">Următoarele %d capitole</item> | ||||
|     </plurals> | ||||
|     <plurals name="missing_chapters"> | ||||
|         <item quantity="one">Lipsește %1$s capitol</item> | ||||
|         <item quantity="few">Lipsesc %1$s capitole</item> | ||||
|         <item quantity="other">Lipsesc %1$s capitole</item> | ||||
|     </plurals> | ||||
|     <plurals name="day"> | ||||
|         <item quantity="one">O zi</item> | ||||
|         <item quantity="few">%d zile</item> | ||||
|         <item quantity="other">%d zile</item> | ||||
|     </plurals> | ||||
| </resources> | ||||
| @@ -173,7 +173,7 @@ | ||||
|     <string name="cookies_cleared">Cookies curățate</string> | ||||
|     <string name="pref_clear_database">Curățați baza de date</string> | ||||
|     <string name="pref_clear_database_summary">Ștergeți istoricul pentru intrările care nu sunt salvate în bibliotecă</string> | ||||
|     <string name="clear_database_confirmation">Ești sigur\? Capitolele citite și progresul intrărilor din afara bibliotecii vor fi pierdute</string> | ||||
|     <string name="clear_database_confirmation">Ești sigur? Capitolele citite și progresul înregistrărilor din afara bibliotecii vor fi pierdute</string> | ||||
|     <string name="clear_database_completed">Înregistrări șterse</string> | ||||
|     <string name="version">Versiune</string> | ||||
|     <string name="pref_enable_acra">Trimite rapoarte pt. eșuări</string> | ||||
| @@ -300,7 +300,6 @@ | ||||
|     <string name="label_more">Mai multe</string> | ||||
|     <string name="action_view_chapters">Vezi capitolele</string> | ||||
|     <string name="action_cancel_all">Anulează tot</string> | ||||
|     <string name="pref_theme_mode">Mod întunecat</string> | ||||
|     <string name="theme_light">Oprită</string> | ||||
|     <string name="theme_dark">Pornită</string> | ||||
|     <string name="theme_system">Tema sistemului</string> | ||||
| @@ -331,7 +330,7 @@ | ||||
|     <string name="action_menu">Meniu</string> | ||||
|     <string name="action_newest">Cele mai recente</string> | ||||
|     <string name="action_oldest">Cele mai vechi</string> | ||||
|     <string name="action_move_to_top">Mută pe primă poziție</string> | ||||
|     <string name="action_move_to_top">Mută pe prima poziție</string> | ||||
|     <string name="action_move_to_bottom">Mută pe ultima poziție</string> | ||||
|     <string name="channel_ext_updates">Actualizări de extensie</string> | ||||
|     <string name="updating_library">Se actualizează biblioteca</string> | ||||
| @@ -527,7 +526,7 @@ | ||||
|     <string name="pref_hide_threshold">Sensibilitatea pentru ascunderea meniului la defilare</string> | ||||
|     <string name="action_show_manga">Afișează intrarea</string> | ||||
|     <string name="ext_installer_shizuku_unavailable_dialog">Instalați și porniți Shizuku pentru a utiliza Shizuku ca instalator de extensii.</string> | ||||
|     <string name="clear_database_source_item_count">%1$d intrări non-bibliotecare în baza de date</string> | ||||
|     <string name="clear_database_source_item_count">%1$d înregistrări în baza de datecare nu aparțin bibliotecii</string> | ||||
|     <string name="download_notifier_split_page_not_found">Pagina %d nu a fost găsită în timpul divizării</string> | ||||
|     <string name="delete_category_confirmation">Doriți să ștergeți categoria \"%s\"\?</string> | ||||
|     <string name="internal_error">InternalError: Verificați jurnalele de accident pentru informații suplimentare</string> | ||||
| @@ -544,7 +543,7 @@ | ||||
|     <string name="wish_list">Lista de dorințe</string> | ||||
|     <string name="complete_list">Lista completă</string> | ||||
|     <string name="action_display_cover_only_grid">Acoperire tip grilaj</string> | ||||
|     <string name="action_move_to_top_all_for_series">Mutați seria în partea de sus</string> | ||||
|     <string name="action_move_to_top_all_for_series">Mutați seria pe prima poziție</string> | ||||
|     <string name="pref_library_update_manga_restriction">Omiteți actualizarea intrărilor</string> | ||||
|     <string name="pref_update_only_started">Care nu au fost începute</string> | ||||
|     <string name="ext_installer_shizuku_stopped">Shizuku nu rulează</string> | ||||
| @@ -583,7 +582,7 @@ | ||||
|     <string name="cant_open_last_read_chapter">Nu se poate deschide ultimul capitol citit</string> | ||||
|     <string name="extension_api_error">Nu s-a putut obține lista de extensii</string> | ||||
|     <string name="ext_install_service_notif">Se instalează extensia…</string> | ||||
|     <string name="ext_installer_pref">Program instalare</string> | ||||
|     <string name="ext_installer_pref">Program de instalare</string> | ||||
|     <string name="pref_create_folder_per_manga_summary">Creează dosare în funcție de titlul intrărilor</string> | ||||
|     <string name="automatic_background">Automat</string> | ||||
|     <string name="on">Pornit</string> | ||||
| @@ -708,4 +707,61 @@ | ||||
|     <string name="action_sort_next_updated">Următoarea actualizare așteptată</string> | ||||
|     <string name="action_filter_interval_long">Fetch lunar (28 de zile)</string> | ||||
|     <string name="action_filter_interval_custom">Interval de preluare personalizat</string> | ||||
|     <string name="pref_storage_location">Locație de stocare</string> | ||||
|     <string name="information_cloudflare_help">Atinge aici pentru ajutor cu Cloudflare</string> | ||||
|     <string name="action_create">Creați</string> | ||||
|     <string name="relative_time_span_never">Niciodată</string> | ||||
|     <string name="pref_library_columns_per_row">%d pe rând</string> | ||||
|     <string name="pref_flash_page_summ">Reduceți imaginile fantomă pe ecranele e-ink</string> | ||||
|     <string name="action_copy_to_clipboard">Copiați în clipboard</string> | ||||
|     <string name="pref_page_rotate">Rotiți paginile late pentru a se potrivi</string> | ||||
|     <string name="pref_storage_location_info">Folosit pentru copii de rezervă automate, capitole descărcate, și surse locale.</string> | ||||
|     <string name="action_apply">Aplică</string> | ||||
|     <string name="pref_debug_info">Informații de depanare</string> | ||||
|     <string name="syncing_library">Sincronizare bibliotecă</string> | ||||
|     <string name="create_backup_file_error">Copia de rezervă nu a putut fi creată</string> | ||||
|     <string name="intervals_header">Intervale</string> | ||||
|     <string name="action_revert_to_default">Reveniți la implicit</string> | ||||
|     <string name="action_sort_category">Sortează categoriile</string> | ||||
|     <string name="action_update_category">Actualizați categoria</string> | ||||
|     <string name="action_menu_overflow_description">Mai multe opțiuni</string> | ||||
|     <string name="library_sync_complete">Sincronizare bibliotecă completă</string> | ||||
|     <string name="last_auto_backup_info">Ultima copie de rezervă creata la: %s</string> | ||||
|     <string name="manga_modify_calculated_interval_title">Personalizați intervalul</string> | ||||
|     <string name="pref_page_rotate_invert">Răsturnați orientarea paginilor late rotite</string> | ||||
|     <string name="selected">Selectați</string> | ||||
|     <string name="no_scanlators_found">Nici un scanlator nu a fost găsit</string> | ||||
|     <string name="not_selected">Nu a fost selectat</string> | ||||
|     <string name="action_move_to_bottom_all_for_series">Mutați seria pe ultima poziție</string> | ||||
|     <string name="pref_chapter_swipe_end">Acțiune glisare către dreapta</string> | ||||
|     <string name="licensed_manga_chapters_error">Licențiat - Nu exista capitole pentru afișare</string> | ||||
|     <string name="exception_offline">Fără conexiune la internet</string> | ||||
|     <string name="pref_storage_usage">Utilizarea spațiului de stocare</string> | ||||
|     <string name="notification_updating_progress">Actualizare bibliotecă… (%s)</string> | ||||
|     <string name="download_cache_invalidated">Indexul de descărcări a fost invalidat</string> | ||||
|     <string name="action_bar_up_description">Navighează în sus</string> | ||||
|     <string name="action_sort_tracker_score">Scorul tracker-ului</string> | ||||
|     <string name="sort_category_confirmation">Ați dori să sortați categoriile alfabetic?</string> | ||||
|     <string name="skipped_reason_not_in_release_period">Sărit peste deoarece nici o lansare nu era așteptată astăzi</string> | ||||
|     <string name="file_null_uri_error">Nici o filă selectată</string> | ||||
|     <string name="track_delete_title">Eliminați monitorizarea %s?</string> | ||||
|     <string name="source_settings">Setări surse</string> | ||||
|     <string name="app_settings">Setări aplicație</string> | ||||
|     <string name="pref_chapter_swipe_start">Acțiune glisare către stânga</string> | ||||
|     <string name="track_delete_remote_text">De asemenea elimină din %s</string> | ||||
|     <string name="split_tall_images">Împărțiți imaginile înalte</string> | ||||
|     <string name="has_results">Au fost găsite rezultate</string> | ||||
|     <string name="track_delete_text">Această acțiune va elimina local monitorizarea.</string> | ||||
|     <string name="pref_update_only_in_release_period">În afara perioadei de lansare estimată</string> | ||||
|     <string name="action_ok">Ok</string> | ||||
|     <string name="pref_double_tap_zoom">Atingeți de două ori pentru a mări</string> | ||||
|     <string name="track_activity_name">Autentificare tracker</string> | ||||
|     <string name="pref_hide_in_library_items">Ascundeți înregistrările care se află deja în bibliotecă</string> | ||||
|     <string name="pref_relative_format">Marcaje de timp relative</string> | ||||
|     <string name="exception_http">HTTP %d, verificați site-ul in modul WebView</string> | ||||
|     <string name="pref_relative_format_summary">\"%1$s\" în loc de \"%2$s\"</string> | ||||
|     <string name="exception_unknown_host">Nu a putut fi accesat %s</string> | ||||
|     <string name="label_tracked_titles">Înregistrări monitorizate</string> | ||||
|     <string name="exclude_scanlators">Exclude scanlator</string> | ||||
|     <string name="pref_chapter_swipe">Glisare capitol</string> | ||||
| </resources> | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user