Merge branch 'master' into sync-part-final

This commit is contained in:
KaiserBh
2023-12-15 07:01:37 +11:00
committed by GitHub
121 changed files with 900 additions and 447 deletions

View File

@@ -22,7 +22,7 @@ android {
defaultConfig {
applicationId = "eu.kanade.tachiyomi"
versionCode = 111
versionCode = 112
versionName = "0.14.7"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")

View File

@@ -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),

View File

@@ -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

View File

@@ -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(

View File

@@ -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()

View File

@@ -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),
)
}
}
}
}
}
}

View File

@@ -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(

View File

@@ -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,

View File

@@ -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(

View File

@@ -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 = {},
)
}
}

View File

@@ -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]()
}
}
}
}

View File

@@ -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))
}
}
}

View File

@@ -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) },
)
}
}

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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))
}
}
}
},
)
}

View File

@@ -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) }
},
)
}

View File

@@ -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(

View File

@@ -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
}

View File

@@ -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))

View File

@@ -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,

View File

@@ -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 ->

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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.
*

View File

@@ -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) {

View File

@@ -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)
}
}

View File

@@ -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)

View File

@@ -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) },

View File

@@ -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(),
),
)
}

View File

@@ -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()
},
)

View File

@@ -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()
}
}
}
}
}

View File

@@ -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) },
),
),
)

View File

@@ -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.
*

View File

@@ -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,

View File

@@ -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 }

View File

@@ -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)) },
)
}
}

View File

@@ -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))
},
)
}
}

View File

@@ -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() {

View File

@@ -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()
}

View File

@@ -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,

View File

@@ -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)
}
}

View File

@@ -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 }

View File

@@ -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
}

View File

@@ -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

View File

@@ -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>