mirror of
https://github.com/mihonapp/mihon.git
synced 2024-11-15 15:02:49 +01:00
chore: merge upstream changes.
Signed-off-by: KaiserBh <kaiserbh@proton.me>
This commit is contained in:
commit
80570a823f
@ -2,7 +2,6 @@
|
|||||||
|-------|----------|---------|------------|---------|
|
|-------|----------|---------|------------|---------|
|
||||||
| [![CI](https://github.com/tachiyomiorg/tachiyomi/actions/workflows/build_push.yml/badge.svg)](https://github.com/tachiyomiorg/tachiyomi/actions/workflows/build_push.yml) | [![stable release](https://img.shields.io/github/release/tachiyomiorg/tachiyomi.svg?maxAge=3600&label=download)](https://github.com/tachiyomiorg/tachiyomi/releases) | [![latest preview build](https://img.shields.io/github/v/release/tachiyomiorg/tachiyomi-preview.svg?maxAge=3600&label=download)](https://github.com/tachiyomiorg/tachiyomi-preview/releases) | [![Translation status](https://hosted.weblate.org/widgets/tachiyomi/-/svg-badge.svg)](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | [![Discord](https://img.shields.io/discord/349436576037732353.svg?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/tachiyomi) |
|
| [![CI](https://github.com/tachiyomiorg/tachiyomi/actions/workflows/build_push.yml/badge.svg)](https://github.com/tachiyomiorg/tachiyomi/actions/workflows/build_push.yml) | [![stable release](https://img.shields.io/github/release/tachiyomiorg/tachiyomi.svg?maxAge=3600&label=download)](https://github.com/tachiyomiorg/tachiyomi/releases) | [![latest preview build](https://img.shields.io/github/v/release/tachiyomiorg/tachiyomi-preview.svg?maxAge=3600&label=download)](https://github.com/tachiyomiorg/tachiyomi-preview/releases) | [![Translation status](https://hosted.weblate.org/widgets/tachiyomi/-/svg-badge.svg)](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | [![Discord](https://img.shields.io/discord/349436576037732353.svg?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/tachiyomi) |
|
||||||
|
|
||||||
|
|
||||||
# ![app icon](./.github/readme-images/app-icon.png)Tachiyomi
|
# ![app icon](./.github/readme-images/app-icon.png)Tachiyomi
|
||||||
Tachiyomi is a free and open source manga reader for Android 6.0 and above.
|
Tachiyomi is a free and open source manga reader for Android 6.0 and above.
|
||||||
|
|
||||||
|
2
app/proguard-rules.pro
vendored
2
app/proguard-rules.pro
vendored
@ -45,7 +45,7 @@
|
|||||||
|
|
||||||
##---------------Begin: proguard configuration for kotlinx.serialization ----------
|
##---------------Begin: proguard configuration for kotlinx.serialization ----------
|
||||||
-keepattributes *Annotation*, InnerClasses
|
-keepattributes *Annotation*, InnerClasses
|
||||||
-dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations
|
-dontnote kotlinx.serialization.** # core serialization annotations
|
||||||
|
|
||||||
# kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer
|
# kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer
|
||||||
-keepclassmembers class kotlinx.serialization.json.** {
|
-keepclassmembers class kotlinx.serialization.json.** {
|
||||||
|
@ -8,7 +8,8 @@
|
|||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
|
|
||||||
<!-- Storage -->
|
<!-- Storage -->
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
|
tools:ignore="ScopedStorage" />
|
||||||
|
|
||||||
<!-- For background jobs -->
|
<!-- For background jobs -->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
@ -20,10 +21,12 @@
|
|||||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
||||||
<!-- To view extension packages in API 30+ -->
|
<!-- To view extension packages in API 30+ -->
|
||||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
|
tools:ignore="QueryAllPackagesPermission" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.READ_APP_SPECIFIC_LOCALES" />
|
<uses-permission android:name="android.permission.READ_APP_SPECIFIC_LOCALES"
|
||||||
|
tools:ignore="ProtectedPermissions" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ class RefreshTracks(
|
|||||||
suspend fun await(mangaId: Long): List<Pair<Tracker?, Throwable>> {
|
suspend fun await(mangaId: Long): List<Pair<Tracker?, Throwable>> {
|
||||||
return supervisorScope {
|
return supervisorScope {
|
||||||
return@supervisorScope getTracks.await(mangaId)
|
return@supervisorScope getTracks.await(mangaId)
|
||||||
.map { it to trackerManager.get(it.syncId) }
|
.map { it to trackerManager.get(it.trackerId) }
|
||||||
.filter { (_, service) -> service?.isLoggedIn == true }
|
.filter { (_, service) -> service?.isLoggedIn == true }
|
||||||
.map { (track, service) ->
|
.map { (track, service) ->
|
||||||
async {
|
async {
|
||||||
|
@ -27,7 +27,7 @@ class TrackChapter(
|
|||||||
if (tracks.isEmpty()) return@withNonCancellableContext
|
if (tracks.isEmpty()) return@withNonCancellableContext
|
||||||
|
|
||||||
tracks.mapNotNull { track ->
|
tracks.mapNotNull { track ->
|
||||||
val service = trackerManager.get(track.syncId)
|
val service = trackerManager.get(track.trackerId)
|
||||||
if (service == null || !service.isLoggedIn || chapterNumber <= track.lastChapterRead) {
|
if (service == null || !service.isLoggedIn || chapterNumber <= track.lastChapterRead) {
|
||||||
return@mapNotNull null
|
return@mapNotNull null
|
||||||
}
|
}
|
||||||
|
@ -13,10 +13,10 @@ fun Track.copyPersonalFrom(other: Track): Track {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Track.toDbTrack(): DbTrack = DbTrack.create(syncId).also {
|
fun Track.toDbTrack(): DbTrack = DbTrack.create(trackerId).also {
|
||||||
it.id = id
|
it.id = id
|
||||||
it.manga_id = mangaId
|
it.manga_id = mangaId
|
||||||
it.media_id = remoteId
|
it.remote_id = remoteId
|
||||||
it.library_id = libraryId
|
it.library_id = libraryId
|
||||||
it.title = title
|
it.title = title
|
||||||
it.last_chapter_read = lastChapterRead.toFloat()
|
it.last_chapter_read = lastChapterRead.toFloat()
|
||||||
@ -33,8 +33,8 @@ fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? {
|
|||||||
return Track(
|
return Track(
|
||||||
id = trackId,
|
id = trackId,
|
||||||
mangaId = manga_id,
|
mangaId = manga_id,
|
||||||
syncId = sync_id.toLong(),
|
trackerId = tracker_id.toLong(),
|
||||||
remoteId = media_id,
|
remoteId = remote_id,
|
||||||
libraryId = library_id,
|
libraryId = library_id,
|
||||||
title = title,
|
title = title,
|
||||||
lastChapterRead = last_chapter_read.toDouble(),
|
lastChapterRead = last_chapter_read.toDouble(),
|
||||||
|
@ -9,26 +9,24 @@ class TrackPreferences(
|
|||||||
private val preferenceStore: PreferenceStore,
|
private val preferenceStore: PreferenceStore,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun trackUsername(sync: Tracker) = preferenceStore.getString(trackUsername(sync.id), "")
|
fun trackUsername(tracker: Tracker) = preferenceStore.getString(
|
||||||
|
Preference.privateKey("pref_mangasync_username_${tracker.id}"),
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
fun trackPassword(sync: Tracker) = preferenceStore.getString(trackPassword(sync.id), "")
|
fun trackPassword(tracker: Tracker) = preferenceStore.getString(
|
||||||
|
Preference.privateKey("pref_mangasync_password_${tracker.id}"),
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
fun setCredentials(sync: Tracker, username: String, password: String) {
|
fun setCredentials(tracker: Tracker, username: String, password: String) {
|
||||||
trackUsername(sync).set(username)
|
trackUsername(tracker).set(username)
|
||||||
trackPassword(sync).set(password)
|
trackPassword(tracker).set(password)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun trackToken(sync: Tracker) = preferenceStore.getString(trackToken(sync.id), "")
|
fun trackToken(tracker: Tracker) = preferenceStore.getString(Preference.privateKey("track_token_${tracker.id}"), "")
|
||||||
|
|
||||||
fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10)
|
fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10)
|
||||||
|
|
||||||
fun autoUpdateTrack() = preferenceStore.getBoolean("pref_auto_update_manga_sync_key", true)
|
fun autoUpdateTrack() = preferenceStore.getBoolean("pref_auto_update_manga_sync_key", true)
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun trackUsername(syncId: Long) = Preference.privateKey("pref_mangasync_username_$syncId")
|
|
||||||
|
|
||||||
private fun trackPassword(syncId: Long) = Preference.privateKey("pref_mangasync_password_$syncId")
|
|
||||||
|
|
||||||
private fun trackToken(syncId: Long) = Preference.privateKey("track_token_$syncId")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,7 @@ import androidx.compose.ui.focus.FocusRequester
|
|||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import eu.kanade.core.preference.asToggleableState
|
import eu.kanade.core.preference.asToggleableState
|
||||||
import eu.kanade.presentation.category.visualName
|
import eu.kanade.presentation.category.visualName
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import tachiyomi.core.preference.CheckboxState
|
import tachiyomi.core.preference.CheckboxState
|
||||||
import tachiyomi.domain.category.model.Category
|
import tachiyomi.domain.category.model.Category
|
||||||
@ -39,7 +40,7 @@ import kotlin.time.Duration.Companion.seconds
|
|||||||
fun CategoryCreateDialog(
|
fun CategoryCreateDialog(
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onCreate: (String) -> Unit,
|
onCreate: (String) -> Unit,
|
||||||
categories: List<Category>,
|
categories: ImmutableList<Category>,
|
||||||
) {
|
) {
|
||||||
var name by remember { mutableStateOf("") }
|
var name by remember { mutableStateOf("") }
|
||||||
|
|
||||||
@ -98,7 +99,7 @@ fun CategoryCreateDialog(
|
|||||||
fun CategoryRenameDialog(
|
fun CategoryRenameDialog(
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onRename: (String) -> Unit,
|
onRename: (String) -> Unit,
|
||||||
categories: List<Category>,
|
categories: ImmutableList<Category>,
|
||||||
category: Category,
|
category: Category,
|
||||||
) {
|
) {
|
||||||
var name by remember { mutableStateOf(category.name) }
|
var name by remember { mutableStateOf(category.name) }
|
||||||
|
@ -6,6 +6,7 @@ import androidx.compose.material.icons.outlined.Add
|
|||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
@ -16,11 +17,13 @@ import tachiyomi.presentation.core.util.isScrollingUp
|
|||||||
fun CategoryFloatingActionButton(
|
fun CategoryFloatingActionButton(
|
||||||
lazyListState: LazyListState,
|
lazyListState: LazyListState,
|
||||||
onCreate: () -> Unit,
|
onCreate: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
ExtendedFloatingActionButton(
|
ExtendedFloatingActionButton(
|
||||||
text = { Text(text = stringResource(MR.strings.action_add)) },
|
text = { Text(text = stringResource(MR.strings.action_add)) },
|
||||||
icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = null) },
|
icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = null) },
|
||||||
onClick = onCreate,
|
onClick = onCreate,
|
||||||
expanded = lazyListState.isScrollingUp() || lazyListState.isScrolledToEnd(),
|
expanded = lazyListState.isScrollingUp() || lazyListState.isScrolledToEnd(),
|
||||||
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,9 @@ package eu.kanade.presentation.components
|
|||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import eu.kanade.presentation.manga.DownloadAction
|
import eu.kanade.presentation.manga.DownloadAction
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.i18n.pluralStringResource
|
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
@ -13,18 +15,22 @@ fun DownloadDropdownMenu(
|
|||||||
expanded: Boolean,
|
expanded: Boolean,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onDownloadClicked: (DownloadAction) -> Unit,
|
onDownloadClicked: (DownloadAction) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
DropdownMenu(
|
val options = persistentListOf(
|
||||||
expanded = expanded,
|
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
) {
|
|
||||||
listOfNotNull(
|
|
||||||
DownloadAction.NEXT_1_CHAPTER to pluralStringResource(MR.plurals.download_amount, 1, 1),
|
DownloadAction.NEXT_1_CHAPTER to pluralStringResource(MR.plurals.download_amount, 1, 1),
|
||||||
DownloadAction.NEXT_5_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 5, 5),
|
DownloadAction.NEXT_5_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 5, 5),
|
||||||
DownloadAction.NEXT_10_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 10, 10),
|
DownloadAction.NEXT_10_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 10, 10),
|
||||||
DownloadAction.NEXT_25_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 25, 25),
|
DownloadAction.NEXT_25_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 25, 25),
|
||||||
DownloadAction.UNREAD_CHAPTERS to stringResource(MR.strings.download_unread),
|
DownloadAction.UNREAD_CHAPTERS to stringResource(MR.strings.download_unread),
|
||||||
).map { (downloadAction, string) ->
|
)
|
||||||
|
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
options.map { (downloadAction, string) ->
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(text = string) },
|
text = { Text(text = string) },
|
||||||
onClick = {
|
onClick = {
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
package eu.kanade.presentation.components
|
package eu.kanade.presentation.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ScrollState
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.sizeIn
|
import androidx.compose.foundation.layout.sizeIn
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
|
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
|
||||||
import androidx.compose.material.icons.outlined.RadioButtonChecked
|
import androidx.compose.material.icons.outlined.RadioButtonChecked
|
||||||
@ -22,12 +25,17 @@ import tachiyomi.i18n.MR
|
|||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import androidx.compose.material3.DropdownMenu as ComposeDropdownMenu
|
import androidx.compose.material3.DropdownMenu as ComposeDropdownMenu
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DropdownMenu but overlaps anchor and has width constraints to better
|
||||||
|
* match non-Compose implementation.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun DropdownMenu(
|
fun DropdownMenu(
|
||||||
expanded: Boolean,
|
expanded: Boolean,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
offset: DpOffset = DpOffset(8.dp, (-56).dp),
|
offset: DpOffset = DpOffset(8.dp, (-56).dp),
|
||||||
|
scrollState: ScrollState = rememberScrollState(),
|
||||||
properties: PopupProperties = PopupProperties(focusable = true),
|
properties: PopupProperties = PopupProperties(focusable = true),
|
||||||
content: @Composable ColumnScope.() -> Unit,
|
content: @Composable ColumnScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
@ -36,6 +44,7 @@ fun DropdownMenu(
|
|||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
modifier = modifier.sizeIn(minWidth = 196.dp, maxWidth = 196.dp),
|
modifier = modifier.sizeIn(minWidth = 196.dp, maxWidth = 196.dp),
|
||||||
offset = offset,
|
offset = offset,
|
||||||
|
scrollState = scrollState,
|
||||||
properties = properties,
|
properties = properties,
|
||||||
content = content,
|
content = content,
|
||||||
)
|
)
|
||||||
@ -45,6 +54,7 @@ fun DropdownMenu(
|
|||||||
fun RadioMenuItem(
|
fun RadioMenuItem(
|
||||||
text: @Composable () -> Unit,
|
text: @Composable () -> Unit,
|
||||||
isChecked: Boolean,
|
isChecked: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
@ -64,6 +74,7 @@ fun RadioMenuItem(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,10 +82,12 @@ fun RadioMenuItem(
|
|||||||
fun NestedMenuItem(
|
fun NestedMenuItem(
|
||||||
text: @Composable () -> Unit,
|
text: @Composable () -> Unit,
|
||||||
children: @Composable ColumnScope.(() -> Unit) -> Unit,
|
children: @Composable ColumnScope.(() -> Unit) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
var nestedExpanded by remember { mutableStateOf(false) }
|
var nestedExpanded by remember { mutableStateOf(false) }
|
||||||
val closeMenu = { nestedExpanded = false }
|
val closeMenu = { nestedExpanded = false }
|
||||||
|
|
||||||
|
Box {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = text,
|
text = text,
|
||||||
onClick = { nestedExpanded = true },
|
onClick = { nestedExpanded = true },
|
||||||
@ -89,7 +102,9 @@ fun NestedMenuItem(
|
|||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
expanded = nestedExpanded,
|
expanded = nestedExpanded,
|
||||||
onDismissRequest = closeMenu,
|
onDismissRequest = closeMenu,
|
||||||
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
children(closeMenu)
|
children(closeMenu)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,6 +77,7 @@ import tachiyomi.presentation.core.components.material.Scaffold
|
|||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.util.isScrolledToEnd
|
import tachiyomi.presentation.core.util.isScrolledToEnd
|
||||||
import tachiyomi.presentation.core.util.isScrollingUp
|
import tachiyomi.presentation.core.util.isScrollingUp
|
||||||
|
import tachiyomi.source.local.isLocal
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
@ -718,13 +719,13 @@ fun MangaScreenLargeImpl(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun SharedMangaBottomActionMenu(
|
private fun SharedMangaBottomActionMenu(
|
||||||
selected: List<ChapterList.Item>,
|
selected: List<ChapterList.Item>,
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
|
onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
|
||||||
onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit,
|
onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit,
|
||||||
onMarkPreviousAsReadClicked: (Chapter) -> Unit,
|
onMarkPreviousAsReadClicked: (Chapter) -> Unit,
|
||||||
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
|
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
|
||||||
onMultiDeleteClicked: (List<Chapter>) -> Unit,
|
onMultiDeleteClicked: (List<Chapter>) -> Unit,
|
||||||
fillFraction: Float,
|
fillFraction: Float,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
MangaBottomActionMenu(
|
MangaBottomActionMenu(
|
||||||
visible = selected.isNotEmpty(),
|
visible = selected.isNotEmpty(),
|
||||||
@ -752,7 +753,7 @@ private fun SharedMangaBottomActionMenu(
|
|||||||
onDeleteClicked = {
|
onDeleteClicked = {
|
||||||
onMultiDeleteClicked(selected.fastMap { it.chapter })
|
onMultiDeleteClicked(selected.fastMap { it.chapter })
|
||||||
}.takeIf {
|
}.takeIf {
|
||||||
onDownloadChapter != null && selected.fastAny { it.downloadState == Download.State.DOWNLOADED }
|
selected.fastAny { it.downloadState == Download.State.DOWNLOADED }
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -818,7 +819,7 @@ private fun LazyListScope.sharedChapterItems(
|
|||||||
read = item.chapter.read,
|
read = item.chapter.read,
|
||||||
bookmark = item.chapter.bookmark,
|
bookmark = item.chapter.bookmark,
|
||||||
selected = item.selected,
|
selected = item.selected,
|
||||||
downloadIndicatorEnabled = !isAnyChapterSelected,
|
downloadIndicatorEnabled = !isAnyChapterSelected && !manga.isLocal(),
|
||||||
downloadStateProvider = { item.downloadState },
|
downloadStateProvider = { item.downloadState },
|
||||||
downloadProgressProvider = { item.downloadProgress },
|
downloadProgressProvider = { item.downloadProgress },
|
||||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||||
|
@ -49,10 +49,10 @@ enum class ChapterDownloadAction {
|
|||||||
@Composable
|
@Composable
|
||||||
fun ChapterDownloadIndicator(
|
fun ChapterDownloadIndicator(
|
||||||
enabled: Boolean,
|
enabled: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
downloadStateProvider: () -> Download.State,
|
downloadStateProvider: () -> Download.State,
|
||||||
downloadProgressProvider: () -> Int,
|
downloadProgressProvider: () -> Int,
|
||||||
onClick: (ChapterDownloadAction) -> Unit,
|
onClick: (ChapterDownloadAction) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
when (val downloadState = downloadStateProvider()) {
|
when (val downloadState = downloadStateProvider()) {
|
||||||
Download.State.NOT_DOWNLOADED -> NotDownloadedIndicator(
|
Download.State.NOT_DOWNLOADED -> NotDownloadedIndicator(
|
||||||
@ -109,10 +109,10 @@ private fun NotDownloadedIndicator(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun DownloadingIndicator(
|
private fun DownloadingIndicator(
|
||||||
enabled: Boolean,
|
enabled: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
downloadState: Download.State,
|
downloadState: Download.State,
|
||||||
downloadProgressProvider: () -> Int,
|
downloadProgressProvider: () -> Int,
|
||||||
onClick: (ChapterDownloadAction) -> Unit,
|
onClick: (ChapterDownloadAction) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
var isMenuExpanded by remember { mutableStateOf(false) }
|
var isMenuExpanded by remember { mutableStateOf(false) }
|
||||||
Box(
|
Box(
|
||||||
|
@ -182,7 +182,10 @@ private fun RowScope.Button(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
content: (@Composable () -> Unit)? = null,
|
content: (@Composable () -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val animatedWeight by animateFloatAsState(if (toConfirm) 2f else 1f)
|
val animatedWeight by animateFloatAsState(
|
||||||
|
targetValue = if (toConfirm) 2f else 1f,
|
||||||
|
label = "weight",
|
||||||
|
)
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(48.dp)
|
.size(48.dp)
|
||||||
|
@ -203,18 +203,16 @@ fun MangaChapterListItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onDownloadClick != null) {
|
|
||||||
ChapterDownloadIndicator(
|
ChapterDownloadIndicator(
|
||||||
enabled = downloadIndicatorEnabled,
|
enabled = downloadIndicatorEnabled,
|
||||||
modifier = Modifier.padding(start = 4.dp),
|
modifier = Modifier.padding(start = 4.dp),
|
||||||
downloadStateProvider = downloadStateProvider,
|
downloadStateProvider = downloadStateProvider,
|
||||||
downloadProgressProvider = downloadProgressProvider,
|
downloadProgressProvider = downloadProgressProvider,
|
||||||
onClick = onDownloadClick,
|
onClick = { onDownloadClick?.invoke(it) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSwipeAction(
|
private fun getSwipeAction(
|
||||||
|
@ -20,7 +20,6 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
|
@ -20,6 +20,7 @@ import tachiyomi.presentation.core.i18n.stringResource
|
|||||||
internal class GuidesStep(
|
internal class GuidesStep(
|
||||||
private val onRestoreBackup: () -> Unit,
|
private val onRestoreBackup: () -> Unit,
|
||||||
) : OnboardingStep {
|
) : OnboardingStep {
|
||||||
|
|
||||||
override val isComplete: Boolean = true
|
override val isComplete: Boolean = true
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -4,6 +4,8 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import eu.kanade.tachiyomi.data.track.Tracker
|
import eu.kanade.tachiyomi.data.track.Tracker
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.ImmutableMap
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.core.preference.Preference as PreferenceData
|
import tachiyomi.core.preference.Preference as PreferenceData
|
||||||
@ -64,20 +66,20 @@ sealed class Preference {
|
|||||||
val pref: PreferenceData<T>,
|
val pref: PreferenceData<T>,
|
||||||
override val title: String,
|
override val title: String,
|
||||||
override val subtitle: String? = "%s",
|
override val subtitle: String? = "%s",
|
||||||
val subtitleProvider: @Composable (value: T, entries: Map<T, String>) -> String? =
|
val subtitleProvider: @Composable (value: T, entries: ImmutableMap<T, String>) -> String? =
|
||||||
{ v, e -> subtitle?.format(e[v]) },
|
{ v, e -> subtitle?.format(e[v]) },
|
||||||
override val icon: ImageVector? = null,
|
override val icon: ImageVector? = null,
|
||||||
override val enabled: Boolean = true,
|
override val enabled: Boolean = true,
|
||||||
override val onValueChanged: suspend (newValue: T) -> Boolean = { true },
|
override val onValueChanged: suspend (newValue: T) -> Boolean = { true },
|
||||||
|
|
||||||
val entries: Map<T, String>,
|
val entries: ImmutableMap<T, String>,
|
||||||
) : PreferenceItem<T>() {
|
) : PreferenceItem<T>() {
|
||||||
internal fun internalSet(newValue: Any) = pref.set(newValue as T)
|
internal fun internalSet(newValue: Any) = pref.set(newValue as T)
|
||||||
internal suspend fun internalOnValueChanged(newValue: Any) = onValueChanged(newValue as T)
|
internal suspend fun internalOnValueChanged(newValue: Any) = onValueChanged(newValue as T)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun internalSubtitleProvider(value: Any?, entries: Map<out Any?, String>) =
|
internal fun internalSubtitleProvider(value: Any?, entries: ImmutableMap<out Any?, String>) =
|
||||||
subtitleProvider(value as T, entries as Map<T, String>)
|
subtitleProvider(value as T, entries as ImmutableMap<T, String>)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -87,13 +89,13 @@ sealed class Preference {
|
|||||||
val value: String,
|
val value: String,
|
||||||
override val title: String,
|
override val title: String,
|
||||||
override val subtitle: String? = "%s",
|
override val subtitle: String? = "%s",
|
||||||
val subtitleProvider: @Composable (value: String, entries: Map<String, String>) -> String? =
|
val subtitleProvider: @Composable (value: String, entries: ImmutableMap<String, String>) -> String? =
|
||||||
{ v, e -> subtitle?.format(e[v]) },
|
{ v, e -> subtitle?.format(e[v]) },
|
||||||
override val icon: ImageVector? = null,
|
override val icon: ImageVector? = null,
|
||||||
override val enabled: Boolean = true,
|
override val enabled: Boolean = true,
|
||||||
override val onValueChanged: suspend (newValue: String) -> Boolean = { true },
|
override val onValueChanged: suspend (newValue: String) -> Boolean = { true },
|
||||||
|
|
||||||
val entries: Map<String, String>,
|
val entries: ImmutableMap<String, String>,
|
||||||
) : PreferenceItem<String>()
|
) : PreferenceItem<String>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -104,7 +106,10 @@ sealed class Preference {
|
|||||||
val pref: PreferenceData<Set<String>>,
|
val pref: PreferenceData<Set<String>>,
|
||||||
override val title: String,
|
override val title: String,
|
||||||
override val subtitle: String? = "%s",
|
override val subtitle: String? = "%s",
|
||||||
val subtitleProvider: @Composable (value: Set<String>, entries: Map<String, String>) -> String? = { v, e ->
|
val subtitleProvider: @Composable (
|
||||||
|
value: Set<String>,
|
||||||
|
entries: ImmutableMap<String, String>,
|
||||||
|
) -> String? = { v, e ->
|
||||||
val combined = remember(v) {
|
val combined = remember(v) {
|
||||||
v.map { e[it] }
|
v.map { e[it] }
|
||||||
.takeIf { it.isNotEmpty() }
|
.takeIf { it.isNotEmpty() }
|
||||||
@ -116,7 +121,7 @@ sealed class Preference {
|
|||||||
override val enabled: Boolean = true,
|
override val enabled: Boolean = true,
|
||||||
override val onValueChanged: suspend (newValue: Set<String>) -> Boolean = { true },
|
override val onValueChanged: suspend (newValue: Set<String>) -> Boolean = { true },
|
||||||
|
|
||||||
val entries: Map<String, String>,
|
val entries: ImmutableMap<String, String>,
|
||||||
) : PreferenceItem<Set<String>>()
|
) : PreferenceItem<Set<String>>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -170,6 +175,6 @@ sealed class Preference {
|
|||||||
override val title: String,
|
override val title: String,
|
||||||
override val enabled: Boolean = true,
|
override val enabled: Boolean = true,
|
||||||
|
|
||||||
val preferenceItems: List<PreferenceItem<out Any>>,
|
val preferenceItems: ImmutableList<PreferenceItem<out Any>>,
|
||||||
) : Preference()
|
) : Preference()
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,6 @@ import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
|
|||||||
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
|
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
|
||||||
import eu.kanade.presentation.more.settings.widget.TrackingPreferenceWidget
|
import eu.kanade.presentation.more.settings.widget.TrackingPreferenceWidget
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import tachiyomi.core.preference.PreferenceStore
|
|
||||||
import tachiyomi.presentation.core.components.SliderItem
|
import tachiyomi.presentation.core.components.SliderItem
|
||||||
import tachiyomi.presentation.core.util.collectAsState
|
import tachiyomi.presentation.core.util.collectAsState
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
@ -157,8 +156,8 @@ internal fun PreferenceItem(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
is Preference.PreferenceItem.TrackerPreference -> {
|
is Preference.PreferenceItem.TrackerPreference -> {
|
||||||
val uName by Injekt.get<PreferenceStore>()
|
val uName by Injekt.get<TrackPreferences>()
|
||||||
.getString(TrackPreferences.trackUsername(item.tracker.id))
|
.trackUsername(item.tracker)
|
||||||
.collectAsState()
|
.collectAsState()
|
||||||
item.tracker.run {
|
item.tracker.run {
|
||||||
TrackingPreferenceWidget(
|
TrackingPreferenceWidget(
|
||||||
|
@ -11,7 +11,6 @@ import tachiyomi.presentation.core.i18n.stringResource
|
|||||||
/**
|
/**
|
||||||
* Returns a string of categories name for settings subtitle
|
* Returns a string of categories name for settings subtitle
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ReadOnlyComposable
|
@ReadOnlyComposable
|
||||||
@Composable
|
@Composable
|
||||||
fun getCategoriesLabel(
|
fun getCategoriesLabel(
|
||||||
|
@ -51,6 +51,9 @@ import eu.kanade.tachiyomi.util.system.isShizukuInstalled
|
|||||||
import eu.kanade.tachiyomi.util.system.powerManager
|
import eu.kanade.tachiyomi.util.system.powerManager
|
||||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableMap
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
@ -149,7 +152,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.label_background_activity),
|
title = stringResource(MR.strings.label_background_activity),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = stringResource(MR.strings.pref_disable_battery_optimization),
|
title = stringResource(MR.strings.pref_disable_battery_optimization),
|
||||||
subtitle = stringResource(MR.strings.pref_disable_battery_optimization_summary),
|
subtitle = stringResource(MR.strings.pref_disable_battery_optimization_summary),
|
||||||
@ -188,7 +191,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.label_data),
|
title = stringResource(MR.strings.label_data),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = stringResource(MR.strings.pref_invalidate_download_cache),
|
title = stringResource(MR.strings.pref_invalidate_download_cache),
|
||||||
subtitle = stringResource(MR.strings.pref_invalidate_download_cache_summary),
|
subtitle = stringResource(MR.strings.pref_invalidate_download_cache_summary),
|
||||||
@ -218,7 +221,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.label_network),
|
title = stringResource(MR.strings.label_network),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = stringResource(MR.strings.pref_clear_cookies),
|
title = stringResource(MR.strings.pref_clear_cookies),
|
||||||
onClick = {
|
onClick = {
|
||||||
@ -249,7 +252,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = networkPreferences.dohProvider(),
|
pref = networkPreferences.dohProvider(),
|
||||||
title = stringResource(MR.strings.pref_dns_over_https),
|
title = stringResource(MR.strings.pref_dns_over_https),
|
||||||
entries = mapOf(
|
entries = persistentMapOf(
|
||||||
-1 to stringResource(MR.strings.disabled),
|
-1 to stringResource(MR.strings.disabled),
|
||||||
PREF_DOH_CLOUDFLARE to "Cloudflare",
|
PREF_DOH_CLOUDFLARE to "Cloudflare",
|
||||||
PREF_DOH_GOOGLE to "Google",
|
PREF_DOH_GOOGLE to "Google",
|
||||||
@ -302,7 +305,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.label_library),
|
title = stringResource(MR.strings.label_library),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = stringResource(MR.strings.pref_refresh_library_covers),
|
title = stringResource(MR.strings.pref_refresh_library_covers),
|
||||||
onClick = { MetadataUpdateJob.startNow(context) },
|
onClick = { MetadataUpdateJob.startNow(context) },
|
||||||
@ -362,12 +365,13 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||||||
}
|
}
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.label_extensions),
|
title = stringResource(MR.strings.label_extensions),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = extensionInstallerPref,
|
pref = extensionInstallerPref,
|
||||||
title = stringResource(MR.strings.ext_installer_pref),
|
title = stringResource(MR.strings.ext_installer_pref),
|
||||||
entries = extensionInstallerPref.entries
|
entries = extensionInstallerPref.entries
|
||||||
.associateWith { stringResource(it.titleRes) },
|
.associateWith { stringResource(it.titleRes) }
|
||||||
|
.toImmutableMap(),
|
||||||
onValueChanged = {
|
onValueChanged = {
|
||||||
if (it == BasePreferences.ExtensionInstaller.SHIZUKU &&
|
if (it == BasePreferences.ExtensionInstaller.SHIZUKU &&
|
||||||
!context.isShizukuInstalled
|
!context.isShizukuInstalled
|
||||||
|
@ -24,6 +24,9 @@ import eu.kanade.presentation.more.settings.widget.AppThemePreferenceWidget
|
|||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
|
import kotlinx.collections.immutable.ImmutableMap
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableMap
|
||||||
import org.xmlpull.v1.XmlPullParser
|
import org.xmlpull.v1.XmlPullParser
|
||||||
import tachiyomi.core.i18n.stringResource
|
import tachiyomi.core.i18n.stringResource
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
@ -66,7 +69,7 @@ object SettingsAppearanceScreen : SearchableSettings {
|
|||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_category_theme),
|
title = stringResource(MR.strings.pref_category_theme),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.CustomPreference(
|
Preference.PreferenceItem.CustomPreference(
|
||||||
title = stringResource(MR.strings.pref_app_theme),
|
title = stringResource(MR.strings.pref_app_theme),
|
||||||
) {
|
) {
|
||||||
@ -127,7 +130,7 @@ object SettingsAppearanceScreen : SearchableSettings {
|
|||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_category_display),
|
title = stringResource(MR.strings.pref_category_display),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.BasicListPreference(
|
Preference.PreferenceItem.BasicListPreference(
|
||||||
value = currentLanguage,
|
value = currentLanguage,
|
||||||
title = stringResource(MR.strings.pref_app_language),
|
title = stringResource(MR.strings.pref_app_language),
|
||||||
@ -140,7 +143,9 @@ object SettingsAppearanceScreen : SearchableSettings {
|
|||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = uiPreferences.tabletUiMode(),
|
pref = uiPreferences.tabletUiMode(),
|
||||||
title = stringResource(MR.strings.pref_tablet_ui_mode),
|
title = stringResource(MR.strings.pref_tablet_ui_mode),
|
||||||
entries = TabletUiMode.entries.associateWith { stringResource(it.titleRes) },
|
entries = TabletUiMode.entries
|
||||||
|
.associateWith { stringResource(it.titleRes) }
|
||||||
|
.toImmutableMap(),
|
||||||
onValueChanged = {
|
onValueChanged = {
|
||||||
context.toast(MR.strings.requires_app_restart)
|
context.toast(MR.strings.requires_app_restart)
|
||||||
true
|
true
|
||||||
@ -153,7 +158,8 @@ object SettingsAppearanceScreen : SearchableSettings {
|
|||||||
.associateWith {
|
.associateWith {
|
||||||
val formattedDate = UiPreferences.dateFormat(it).format(now)
|
val formattedDate = UiPreferences.dateFormat(it).format(now)
|
||||||
"${it.ifEmpty { stringResource(MR.strings.label_default) }} ($formattedDate)"
|
"${it.ifEmpty { stringResource(MR.strings.label_default) }} ($formattedDate)"
|
||||||
},
|
}
|
||||||
|
.toImmutableMap(),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
pref = uiPreferences.relativeTime(),
|
pref = uiPreferences.relativeTime(),
|
||||||
@ -167,7 +173,7 @@ object SettingsAppearanceScreen : SearchableSettings {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
private fun getLangs(context: Context): Map<String, String> {
|
private fun getLangs(context: Context): ImmutableMap<String, String> {
|
||||||
val langs = mutableListOf<Pair<String, String>>()
|
val langs = mutableListOf<Pair<String, String>>()
|
||||||
val parser = context.resources.getXml(R.xml.locales_config)
|
val parser = context.resources.getXml(R.xml.locales_config)
|
||||||
var eventType = parser.eventType
|
var eventType = parser.eventType
|
||||||
@ -189,7 +195,7 @@ object SettingsAppearanceScreen : SearchableSettings {
|
|||||||
langs.sortBy { it.second }
|
langs.sortBy { it.second }
|
||||||
langs.add(0, Pair("", context.stringResource(MR.strings.label_default)))
|
langs.add(0, Pair("", context.stringResource(MR.strings.label_default)))
|
||||||
|
|
||||||
return langs.toMap()
|
return langs.toMap().toImmutableMap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import androidx.fragment.app.FragmentActivity
|
|||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.presentation.more.settings.Preference
|
import eu.kanade.presentation.more.settings.Preference
|
||||||
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
|
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import tachiyomi.core.i18n.stringResource
|
import tachiyomi.core.i18n.stringResource
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
@ -27,7 +28,7 @@ object SettingsBrowseScreen : SearchableSettings {
|
|||||||
return listOf(
|
return listOf(
|
||||||
Preference.PreferenceGroup(
|
Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.label_sources),
|
title = stringResource(MR.strings.label_sources),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
pref = sourcePreferences.hideInLibraryItems(),
|
pref = sourcePreferences.hideInLibraryItems(),
|
||||||
title = stringResource(MR.strings.pref_hide_in_library_items),
|
title = stringResource(MR.strings.pref_hide_in_library_items),
|
||||||
@ -36,7 +37,7 @@ object SettingsBrowseScreen : SearchableSettings {
|
|||||||
),
|
),
|
||||||
Preference.PreferenceGroup(
|
Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_category_nsfw_content),
|
title = stringResource(MR.strings.pref_category_nsfw_content),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
pref = sourcePreferences.showNsfwSource(),
|
pref = sourcePreferences.showNsfwSource(),
|
||||||
title = stringResource(MR.strings.pref_show_nsfw_source),
|
title = stringResource(MR.strings.pref_show_nsfw_source),
|
||||||
|
@ -1,21 +1,17 @@
|
|||||||
package eu.kanade.presentation.more.settings.screen
|
package eu.kanade.presentation.more.settings.screen
|
||||||
|
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
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.ManagedActivityResultLauncher
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.MultiChoiceSegmentedButtonRow
|
||||||
|
import androidx.compose.material3.SegmentedButton
|
||||||
|
import androidx.compose.material3.SegmentedButtonDefaults
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@ -34,12 +30,12 @@ import cafe.adriel.voyager.navigator.currentOrThrow
|
|||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.presentation.more.settings.Preference
|
import eu.kanade.presentation.more.settings.Preference
|
||||||
import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen
|
import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen
|
||||||
|
import eu.kanade.presentation.more.settings.screen.data.RestoreBackupScreen
|
||||||
|
import eu.kanade.presentation.more.settings.screen.data.StorageInfo
|
||||||
import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget
|
import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget
|
||||||
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
|
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
|
||||||
import eu.kanade.presentation.util.relativeTimeSpanString
|
import eu.kanade.presentation.util.relativeTimeSpanString
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
|
|
||||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
|
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
|
||||||
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
|
|
||||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
import eu.kanade.tachiyomi.data.sync.SyncDataJob
|
import eu.kanade.tachiyomi.data.sync.SyncDataJob
|
||||||
import eu.kanade.tachiyomi.data.sync.SyncManager
|
import eu.kanade.tachiyomi.data.sync.SyncManager
|
||||||
@ -49,6 +45,8 @@ import eu.kanade.tachiyomi.util.storage.DiskUtil
|
|||||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.i18n.stringResource
|
import tachiyomi.core.i18n.stringResource
|
||||||
@ -67,6 +65,8 @@ import uy.kohesive.injekt.api.get
|
|||||||
|
|
||||||
object SettingsDataScreen : SearchableSettings {
|
object SettingsDataScreen : SearchableSettings {
|
||||||
|
|
||||||
|
val restorePreferenceKeyString = MR.strings.label_backup
|
||||||
|
|
||||||
@ReadOnlyComposable
|
@ReadOnlyComposable
|
||||||
@Composable
|
@Composable
|
||||||
override fun getTitleRes() = MR.strings.label_data_storage
|
override fun getTitleRes() = MR.strings.label_data_storage
|
||||||
@ -79,7 +79,7 @@ object SettingsDataScreen : SearchableSettings {
|
|||||||
val syncPreferences = remember { Injekt.get<SyncPreferences>() }
|
val syncPreferences = remember { Injekt.get<SyncPreferences>() }
|
||||||
val syncService by syncPreferences.syncService().collectAsState()
|
val syncService by syncPreferences.syncService().collectAsState()
|
||||||
|
|
||||||
return listOf(
|
return persistentListOf(
|
||||||
getStorageLocationPref(storagePreferences = storagePreferences),
|
getStorageLocationPref(storagePreferences = storagePreferences),
|
||||||
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.pref_storage_location_info)),
|
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.pref_storage_location_info)),
|
||||||
|
|
||||||
@ -150,20 +150,48 @@ object SettingsDataScreen : SearchableSettings {
|
|||||||
@Composable
|
@Composable
|
||||||
private fun getBackupAndRestoreGroup(backupPreferences: BackupPreferences): Preference.PreferenceGroup {
|
private fun getBackupAndRestoreGroup(backupPreferences: BackupPreferences): Preference.PreferenceGroup {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
|
||||||
val lastAutoBackup by backupPreferences.lastAutoBackupTimestamp().collectAsState()
|
val lastAutoBackup by backupPreferences.lastAutoBackupTimestamp().collectAsState()
|
||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.label_backup),
|
title = stringResource(MR.strings.label_backup),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
// Manual actions
|
// Manual actions
|
||||||
getCreateBackupPref(),
|
Preference.PreferenceItem.CustomPreference(
|
||||||
getRestoreBackupPref(),
|
title = stringResource(restorePreferenceKeyString),
|
||||||
|
) {
|
||||||
|
BasePreferenceWidget(
|
||||||
|
subcomponent = {
|
||||||
|
MultiChoiceSegmentedButtonRow(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = PrefsHorizontalPadding),
|
||||||
|
) {
|
||||||
|
SegmentedButton(
|
||||||
|
checked = false,
|
||||||
|
onCheckedChange = { navigator.push(CreateBackupScreen()) },
|
||||||
|
shape = SegmentedButtonDefaults.itemShape(0, 2),
|
||||||
|
) {
|
||||||
|
Text(stringResource(MR.strings.pref_create_backup))
|
||||||
|
}
|
||||||
|
SegmentedButton(
|
||||||
|
checked = false,
|
||||||
|
onCheckedChange = { navigator.push(RestoreBackupScreen()) },
|
||||||
|
shape = SegmentedButtonDefaults.itemShape(1, 2),
|
||||||
|
) {
|
||||||
|
Text(stringResource(MR.strings.pref_restore_backup))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
// Automatic backups
|
// Automatic backups
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = backupPreferences.backupInterval(),
|
pref = backupPreferences.backupInterval(),
|
||||||
title = stringResource(MR.strings.pref_backup_interval),
|
title = stringResource(MR.strings.pref_backup_interval),
|
||||||
entries = mapOf(
|
entries = persistentMapOf(
|
||||||
0 to stringResource(MR.strings.off),
|
0 to stringResource(MR.strings.off),
|
||||||
6 to stringResource(MR.strings.update_6hour),
|
6 to stringResource(MR.strings.update_6hour),
|
||||||
12 to stringResource(MR.strings.update_12hour),
|
12 to stringResource(MR.strings.update_12hour),
|
||||||
@ -184,142 +212,10 @@ object SettingsDataScreen : SearchableSettings {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun getCreateBackupPref(): Preference.PreferenceItem.TextPreference {
|
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
|
||||||
return Preference.PreferenceItem.TextPreference(
|
|
||||||
title = stringResource(MR.strings.pref_create_backup),
|
|
||||||
subtitle = stringResource(MR.strings.pref_create_backup_summ),
|
|
||||||
onClick = { navigator.push(CreateBackupScreen()) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun getRestoreBackupPref(): Preference.PreferenceItem.TextPreference {
|
|
||||||
val context = LocalContext.current
|
|
||||||
var error by remember { mutableStateOf<Any?>(null) }
|
|
||||||
if (error != null) {
|
|
||||||
val onDismissRequest = { error = null }
|
|
||||||
when (val err = error) {
|
|
||||||
is InvalidRestore -> {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
title = { Text(text = stringResource(MR.strings.invalid_backup_file)) },
|
|
||||||
text = { Text(text = listOfNotNull(err.uri, err.message).joinToString("\n\n")) },
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
context.copyToClipboard(err.message, err.message)
|
|
||||||
onDismissRequest()
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Text(text = stringResource(MR.strings.action_copy_to_clipboard))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = onDismissRequest) {
|
|
||||||
Text(text = stringResource(MR.strings.action_ok))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is MissingRestoreComponents -> {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
title = { Text(text = stringResource(MR.strings.pref_restore_backup)) },
|
|
||||||
text = {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
|
||||||
) {
|
|
||||||
val msg = buildString {
|
|
||||||
append(stringResource(MR.strings.backup_restore_content_full))
|
|
||||||
if (err.sources.isNotEmpty()) {
|
|
||||||
append("\n\n").append(stringResource(MR.strings.backup_restore_missing_sources))
|
|
||||||
err.sources.joinTo(
|
|
||||||
this,
|
|
||||||
separator = "\n- ",
|
|
||||||
prefix = "\n- ",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (err.trackers.isNotEmpty()) {
|
|
||||||
append(
|
|
||||||
"\n\n",
|
|
||||||
).append(stringResource(MR.strings.backup_restore_missing_trackers))
|
|
||||||
err.trackers.joinTo(
|
|
||||||
this,
|
|
||||||
separator = "\n- ",
|
|
||||||
prefix = "\n- ",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Text(text = msg)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
BackupRestoreJob.start(context, err.uri)
|
|
||||||
onDismissRequest()
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Text(text = stringResource(MR.strings.action_restore))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> error = null // Unknown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val chooseBackup = rememberLauncherForActivityResult(
|
|
||||||
object : ActivityResultContracts.GetContent() {
|
|
||||||
override fun createIntent(context: Context, input: String): Intent {
|
|
||||||
val intent = super.createIntent(context, input)
|
|
||||||
return Intent.createChooser(intent, context.stringResource(MR.strings.file_select_backup))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
if (it == null) {
|
|
||||||
context.toast(MR.strings.file_null_uri_error)
|
|
||||||
return@rememberLauncherForActivityResult
|
|
||||||
}
|
|
||||||
|
|
||||||
val results = try {
|
|
||||||
BackupFileValidator().validate(context, it)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
error = InvalidRestore(it, e.message.toString())
|
|
||||||
return@rememberLauncherForActivityResult
|
|
||||||
}
|
|
||||||
|
|
||||||
if (results.missingSources.isEmpty() && results.missingTrackers.isEmpty()) {
|
|
||||||
BackupRestoreJob.start(context, it)
|
|
||||||
return@rememberLauncherForActivityResult
|
|
||||||
}
|
|
||||||
|
|
||||||
error = MissingRestoreComponents(it, results.missingSources, results.missingTrackers)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Preference.PreferenceItem.TextPreference(
|
|
||||||
title = stringResource(MR.strings.pref_restore_backup),
|
|
||||||
subtitle = stringResource(MR.strings.pref_restore_backup_summ),
|
|
||||||
onClick = {
|
|
||||||
if (!BackupRestoreJob.isRunning(context)) {
|
|
||||||
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
|
|
||||||
context.toast(MR.strings.restore_miui_warning, Toast.LENGTH_LONG)
|
|
||||||
}
|
|
||||||
// no need to catch because it's wrapped with a chooser
|
|
||||||
chooseBackup.launch("*/*")
|
|
||||||
} else {
|
|
||||||
context.toast(MR.strings.restore_in_progress)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun getDataGroup(): Preference.PreferenceGroup {
|
private fun getDataGroup(): Preference.PreferenceGroup {
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
val libraryPreferences = remember { Injekt.get<LibraryPreferences>() }
|
val libraryPreferences = remember { Injekt.get<LibraryPreferences>() }
|
||||||
|
|
||||||
val chapterCache = remember { Injekt.get<ChapterCache>() }
|
val chapterCache = remember { Injekt.get<ChapterCache>() }
|
||||||
@ -327,9 +223,19 @@ object SettingsDataScreen : SearchableSettings {
|
|||||||
val cacheReadableSize = remember(cacheReadableSizeSema) { chapterCache.readableSize }
|
val cacheReadableSize = remember(cacheReadableSizeSema) { chapterCache.readableSize }
|
||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.label_data),
|
title = stringResource(MR.strings.pref_storage_usage),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
getStorageInfoPref(cacheReadableSize),
|
Preference.PreferenceItem.CustomPreference(
|
||||||
|
title = stringResource(MR.strings.pref_storage_usage),
|
||||||
|
) {
|
||||||
|
BasePreferenceWidget(
|
||||||
|
subcomponent = {
|
||||||
|
StorageInfo(
|
||||||
|
modifier = Modifier.padding(horizontal = PrefsHorizontalPadding),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = stringResource(MR.strings.pref_clear_chapter_cache),
|
title = stringResource(MR.strings.pref_clear_chapter_cache),
|
||||||
@ -357,43 +263,16 @@ object SettingsDataScreen : SearchableSettings {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun getStorageInfoPref(
|
|
||||||
chapterCacheReadableSize: String,
|
|
||||||
): Preference.PreferenceItem.CustomPreference {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val available = remember {
|
|
||||||
Formatter.formatFileSize(context, DiskUtil.getAvailableStorageSpace(Environment.getDataDirectory()))
|
|
||||||
}
|
|
||||||
val total = remember {
|
|
||||||
Formatter.formatFileSize(context, DiskUtil.getTotalStorageSpace(Environment.getDataDirectory()))
|
|
||||||
}
|
|
||||||
|
|
||||||
return Preference.PreferenceItem.CustomPreference(
|
|
||||||
title = stringResource(MR.strings.pref_storage_usage),
|
|
||||||
) {
|
|
||||||
BasePreferenceWidget(
|
|
||||||
title = stringResource(MR.strings.pref_storage_usage),
|
|
||||||
subcomponent = {
|
|
||||||
// TODO: downloads, SD cards, bar representation?, i18n
|
|
||||||
Box(modifier = Modifier.padding(horizontal = PrefsHorizontalPadding)) {
|
|
||||||
Text(text = "Available: $available / $total (chapter cache: $chapterCacheReadableSize)")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun getSyncPreferences(syncPreferences: SyncPreferences, syncService: Int): List<Preference> {
|
private fun getSyncPreferences(syncPreferences: SyncPreferences, syncService: Int): List<Preference> {
|
||||||
return listOf(
|
return listOf(
|
||||||
Preference.PreferenceGroup(
|
Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_sync_service_category),
|
title = stringResource(MR.strings.pref_sync_service_category),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = syncPreferences.syncService(),
|
pref = syncPreferences.syncService(),
|
||||||
title = stringResource(MR.strings.pref_sync_service),
|
title = stringResource(MR.strings.pref_sync_service),
|
||||||
entries = mapOf(
|
entries = persistentMapOf(
|
||||||
SyncManager.SyncService.NONE.value to stringResource(MR.strings.off),
|
SyncManager.SyncService.NONE.value to stringResource(MR.strings.off),
|
||||||
SyncManager.SyncService.SYNCYOMI.value to stringResource(MR.strings.syncyomi),
|
SyncManager.SyncService.SYNCYOMI.value to stringResource(MR.strings.syncyomi),
|
||||||
SyncManager.SyncService.GOOGLE_DRIVE.value to stringResource(MR.strings.google_drive),
|
SyncManager.SyncService.GOOGLE_DRIVE.value to stringResource(MR.strings.google_drive),
|
||||||
@ -404,10 +283,10 @@ object SettingsDataScreen : SearchableSettings {
|
|||||||
),
|
),
|
||||||
) + getSyncServicePreferences(syncPreferences, syncService)
|
) + getSyncServicePreferences(syncPreferences, syncService)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun getSyncServicePreferences(syncPreferences: SyncPreferences, syncService: Int): List<Preference> {
|
@Composable
|
||||||
|
private fun getSyncServicePreferences(syncPreferences: SyncPreferences, syncService: Int): List<Preference> {
|
||||||
val syncServiceType = SyncManager.SyncService.fromInt(syncService)
|
val syncServiceType = SyncManager.SyncService.fromInt(syncService)
|
||||||
|
|
||||||
val basePreferences = getBasePreferences(syncServiceType, syncPreferences)
|
val basePreferences = getBasePreferences(syncServiceType, syncPreferences)
|
||||||
@ -417,27 +296,27 @@ private fun getSyncServicePreferences(syncPreferences: SyncPreferences, syncServ
|
|||||||
} else {
|
} else {
|
||||||
basePreferences
|
basePreferences
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun getBasePreferences(
|
private fun getBasePreferences(
|
||||||
syncServiceType: SyncManager.SyncService,
|
syncServiceType: SyncManager.SyncService,
|
||||||
syncPreferences: SyncPreferences,
|
syncPreferences: SyncPreferences,
|
||||||
): List<Preference> {
|
): List<Preference> {
|
||||||
return when (syncServiceType) {
|
return when (syncServiceType) {
|
||||||
SyncManager.SyncService.NONE -> emptyList()
|
SyncManager.SyncService.NONE -> emptyList()
|
||||||
SyncManager.SyncService.SYNCYOMI -> getSelfHostPreferences(syncPreferences)
|
SyncManager.SyncService.SYNCYOMI -> getSelfHostPreferences(syncPreferences)
|
||||||
SyncManager.SyncService.GOOGLE_DRIVE -> getGoogleDrivePreferences()
|
SyncManager.SyncService.GOOGLE_DRIVE -> getGoogleDrivePreferences()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun getAdditionalPreferences(syncPreferences: SyncPreferences): List<Preference> {
|
private fun getAdditionalPreferences(syncPreferences: SyncPreferences): List<Preference> {
|
||||||
return listOf(getSyncNowPref(), getAutomaticSyncGroup(syncPreferences))
|
return listOf(getSyncNowPref(), getAutomaticSyncGroup(syncPreferences))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun getGoogleDrivePreferences(): List<Preference> {
|
private fun getGoogleDrivePreferences(): List<Preference> {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val googleDriveSync = Injekt.get<GoogleDriveService>()
|
val googleDriveSync = Injekt.get<GoogleDriveService>()
|
||||||
return listOf(
|
return listOf(
|
||||||
@ -450,10 +329,10 @@ private fun getGoogleDrivePreferences(): List<Preference> {
|
|||||||
),
|
),
|
||||||
getGoogleDrivePurge(),
|
getGoogleDrivePurge(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun getGoogleDrivePurge(): Preference.PreferenceItem.TextPreference {
|
private fun getGoogleDrivePurge(): Preference.PreferenceItem.TextPreference {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val googleDriveSync = remember { GoogleDriveSyncService(context) }
|
val googleDriveSync = remember { GoogleDriveSyncService(context) }
|
||||||
@ -486,13 +365,13 @@ private fun getGoogleDrivePurge(): Preference.PreferenceItem.TextPreference {
|
|||||||
title = stringResource(MR.strings.pref_google_drive_purge_sync_data),
|
title = stringResource(MR.strings.pref_google_drive_purge_sync_data),
|
||||||
onClick = { showPurgeDialog = true },
|
onClick = { showPurgeDialog = true },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PurgeConfirmationDialog(
|
private fun PurgeConfirmationDialog(
|
||||||
onConfirm: () -> Unit,
|
onConfirm: () -> Unit,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
) {
|
) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
title = { Text(text = stringResource(MR.strings.pref_purge_confirmation_title)) },
|
title = { Text(text = stringResource(MR.strings.pref_purge_confirmation_title)) },
|
||||||
@ -508,10 +387,10 @@ private fun PurgeConfirmationDialog(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun getSelfHostPreferences(syncPreferences: SyncPreferences): List<Preference> {
|
private fun getSelfHostPreferences(syncPreferences: SyncPreferences): List<Preference> {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
return listOf(
|
return listOf(
|
||||||
Preference.PreferenceItem.EditTextPreference(
|
Preference.PreferenceItem.EditTextPreference(
|
||||||
@ -534,10 +413,10 @@ private fun getSelfHostPreferences(syncPreferences: SyncPreferences): List<Prefe
|
|||||||
pref = syncPreferences.syncAPIKey(),
|
pref = syncPreferences.syncAPIKey(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun getSyncNowPref(): Preference.PreferenceGroup {
|
private fun getSyncNowPref(): Preference.PreferenceGroup {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
var showDialog by remember { mutableStateOf(false) }
|
var showDialog by remember { mutableStateOf(false) }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@ -558,7 +437,7 @@ private fun getSyncNowPref(): Preference.PreferenceGroup {
|
|||||||
}
|
}
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_sync_now_group_title),
|
title = stringResource(MR.strings.pref_sync_now_group_title),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = stringResource(MR.strings.pref_sync_now),
|
title = stringResource(MR.strings.pref_sync_now),
|
||||||
subtitle = stringResource(MR.strings.pref_sync_now_subtitle),
|
subtitle = stringResource(MR.strings.pref_sync_now_subtitle),
|
||||||
@ -568,21 +447,21 @@ private fun getSyncNowPref(): Preference.PreferenceGroup {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun getAutomaticSyncGroup(syncPreferences: SyncPreferences): Preference.PreferenceGroup {
|
private fun getAutomaticSyncGroup(syncPreferences: SyncPreferences): Preference.PreferenceGroup {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val syncIntervalPref = syncPreferences.syncInterval()
|
val syncIntervalPref = syncPreferences.syncInterval()
|
||||||
val lastSync by syncPreferences.lastSyncTimestamp().collectAsState()
|
val lastSync by syncPreferences.lastSyncTimestamp().collectAsState()
|
||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_sync_automatic_category),
|
title = stringResource(MR.strings.pref_sync_automatic_category),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = syncIntervalPref,
|
pref = syncIntervalPref,
|
||||||
title = stringResource(MR.strings.pref_sync_interval),
|
title = stringResource(MR.strings.pref_sync_interval),
|
||||||
entries = mapOf(
|
entries = persistentMapOf(
|
||||||
0 to stringResource(MR.strings.off),
|
0 to stringResource(MR.strings.off),
|
||||||
30 to stringResource(MR.strings.update_30min),
|
30 to stringResource(MR.strings.update_30min),
|
||||||
60 to stringResource(MR.strings.update_1hour),
|
60 to stringResource(MR.strings.update_1hour),
|
||||||
@ -603,13 +482,13 @@ private fun getAutomaticSyncGroup(syncPreferences: SyncPreferences): Preference.
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SyncConfirmationDialog(
|
private fun SyncConfirmationDialog(
|
||||||
onConfirm: () -> Unit,
|
onConfirm: () -> Unit,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
) {
|
) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
title = { Text(text = stringResource(MR.strings.pref_sync_confirmation_title)) },
|
title = { Text(text = stringResource(MR.strings.pref_sync_confirmation_title)) },
|
||||||
@ -625,15 +504,6 @@ private fun SyncConfirmationDialog(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class MissingRestoreComponents(
|
|
||||||
val uri: Uri,
|
|
||||||
val sources: List<String>,
|
|
||||||
val trackers: List<String>,
|
|
||||||
)
|
|
||||||
|
|
||||||
private data class InvalidRestore(
|
|
||||||
val uri: Uri? = null,
|
|
||||||
val message: String,
|
|
||||||
)
|
|
||||||
|
@ -12,6 +12,9 @@ import androidx.compose.ui.util.fastMap
|
|||||||
import eu.kanade.presentation.category.visualName
|
import eu.kanade.presentation.category.visualName
|
||||||
import eu.kanade.presentation.more.settings.Preference
|
import eu.kanade.presentation.more.settings.Preference
|
||||||
import eu.kanade.presentation.more.settings.widget.TriStateListDialog
|
import eu.kanade.presentation.more.settings.widget.TriStateListDialog
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableMap
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import tachiyomi.domain.category.interactor.GetCategories
|
import tachiyomi.domain.category.interactor.GetCategories
|
||||||
import tachiyomi.domain.category.model.Category
|
import tachiyomi.domain.category.model.Category
|
||||||
@ -68,7 +71,7 @@ object SettingsDownloadScreen : SearchableSettings {
|
|||||||
): Preference.PreferenceGroup {
|
): Preference.PreferenceGroup {
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_category_delete_chapters),
|
title = stringResource(MR.strings.pref_category_delete_chapters),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
pref = downloadPreferences.removeAfterMarkedAsRead(),
|
pref = downloadPreferences.removeAfterMarkedAsRead(),
|
||||||
title = stringResource(MR.strings.pref_remove_after_marked_as_read),
|
title = stringResource(MR.strings.pref_remove_after_marked_as_read),
|
||||||
@ -76,7 +79,7 @@ object SettingsDownloadScreen : SearchableSettings {
|
|||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = downloadPreferences.removeAfterReadSlots(),
|
pref = downloadPreferences.removeAfterReadSlots(),
|
||||||
title = stringResource(MR.strings.pref_remove_after_read),
|
title = stringResource(MR.strings.pref_remove_after_read),
|
||||||
entries = mapOf(
|
entries = persistentMapOf(
|
||||||
-1 to stringResource(MR.strings.disabled),
|
-1 to stringResource(MR.strings.disabled),
|
||||||
0 to stringResource(MR.strings.last_read_chapter),
|
0 to stringResource(MR.strings.last_read_chapter),
|
||||||
1 to stringResource(MR.strings.second_to_last),
|
1 to stringResource(MR.strings.second_to_last),
|
||||||
@ -105,7 +108,9 @@ object SettingsDownloadScreen : SearchableSettings {
|
|||||||
return Preference.PreferenceItem.MultiSelectListPreference(
|
return Preference.PreferenceItem.MultiSelectListPreference(
|
||||||
pref = downloadPreferences.removeExcludeCategories(),
|
pref = downloadPreferences.removeExcludeCategories(),
|
||||||
title = stringResource(MR.strings.pref_remove_exclude_categories),
|
title = stringResource(MR.strings.pref_remove_exclude_categories),
|
||||||
entries = categories().associate { it.id.toString() to it.visualName },
|
entries = categories()
|
||||||
|
.associate { it.id.toString() to it.visualName }
|
||||||
|
.toImmutableMap(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,7 +147,7 @@ object SettingsDownloadScreen : SearchableSettings {
|
|||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_category_auto_download),
|
title = stringResource(MR.strings.pref_category_auto_download),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
pref = downloadNewChaptersPref,
|
pref = downloadNewChaptersPref,
|
||||||
title = stringResource(MR.strings.pref_download_new),
|
title = stringResource(MR.strings.pref_download_new),
|
||||||
@ -167,17 +172,19 @@ object SettingsDownloadScreen : SearchableSettings {
|
|||||||
): Preference.PreferenceGroup {
|
): Preference.PreferenceGroup {
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.download_ahead),
|
title = stringResource(MR.strings.download_ahead),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = downloadPreferences.autoDownloadWhileReading(),
|
pref = downloadPreferences.autoDownloadWhileReading(),
|
||||||
title = stringResource(MR.strings.auto_download_while_reading),
|
title = stringResource(MR.strings.auto_download_while_reading),
|
||||||
entries = listOf(0, 2, 3, 5, 10).associateWith {
|
entries = listOf(0, 2, 3, 5, 10)
|
||||||
|
.associateWith {
|
||||||
if (it == 0) {
|
if (it == 0) {
|
||||||
stringResource(MR.strings.disabled)
|
stringResource(MR.strings.disabled)
|
||||||
} else {
|
} else {
|
||||||
pluralStringResource(MR.plurals.next_unread_chapters, count = it, it)
|
pluralStringResource(MR.plurals.next_unread_chapters, count = it, it)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
.toImmutableMap(),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.download_ahead_info)),
|
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.download_ahead_info)),
|
||||||
),
|
),
|
||||||
|
@ -20,6 +20,9 @@ import eu.kanade.presentation.more.settings.Preference
|
|||||||
import eu.kanade.presentation.more.settings.widget.TriStateListDialog
|
import eu.kanade.presentation.more.settings.widget.TriStateListDialog
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||||
import eu.kanade.tachiyomi.ui.category.CategoryScreen
|
import eu.kanade.tachiyomi.ui.category.CategoryScreen
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableMap
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import tachiyomi.domain.category.interactor.GetCategories
|
import tachiyomi.domain.category.interactor.GetCategories
|
||||||
@ -65,7 +68,6 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||||||
allCategories: List<Category>,
|
allCategories: List<Category>,
|
||||||
libraryPreferences: LibraryPreferences,
|
libraryPreferences: LibraryPreferences,
|
||||||
): Preference.PreferenceGroup {
|
): Preference.PreferenceGroup {
|
||||||
val context = LocalContext.current
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val userCategoriesCount = allCategories.filterNot(Category::isSystemCategory).size
|
val userCategoriesCount = allCategories.filterNot(Category::isSystemCategory).size
|
||||||
|
|
||||||
@ -76,11 +78,11 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||||||
val ids = listOf(libraryPreferences.defaultCategory().defaultValue()) +
|
val ids = listOf(libraryPreferences.defaultCategory().defaultValue()) +
|
||||||
allCategories.fastMap { it.id.toInt() }
|
allCategories.fastMap { it.id.toInt() }
|
||||||
val labels = listOf(stringResource(MR.strings.default_category_summary)) +
|
val labels = listOf(stringResource(MR.strings.default_category_summary)) +
|
||||||
allCategories.fastMap { it.visualName(context) }
|
allCategories.fastMap { it.visualName }
|
||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.categories),
|
title = stringResource(MR.strings.categories),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = stringResource(MR.strings.action_edit_categories),
|
title = stringResource(MR.strings.action_edit_categories),
|
||||||
subtitle = pluralStringResource(
|
subtitle = pluralStringResource(
|
||||||
@ -94,7 +96,7 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||||||
pref = libraryPreferences.defaultCategory(),
|
pref = libraryPreferences.defaultCategory(),
|
||||||
title = stringResource(MR.strings.default_category),
|
title = stringResource(MR.strings.default_category),
|
||||||
subtitle = selectedCategory?.visualName ?: stringResource(MR.strings.default_category_summary),
|
subtitle = selectedCategory?.visualName ?: stringResource(MR.strings.default_category_summary),
|
||||||
entries = ids.zip(labels).toMap(),
|
entries = ids.zip(labels).toMap().toImmutableMap(),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
pref = libraryPreferences.categorizedDisplaySettings(),
|
pref = libraryPreferences.categorizedDisplaySettings(),
|
||||||
@ -147,11 +149,11 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_category_library_update),
|
title = stringResource(MR.strings.pref_category_library_update),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = autoUpdateIntervalPref,
|
pref = autoUpdateIntervalPref,
|
||||||
title = stringResource(MR.strings.pref_library_update_interval),
|
title = stringResource(MR.strings.pref_library_update_interval),
|
||||||
entries = mapOf(
|
entries = persistentMapOf(
|
||||||
0 to stringResource(MR.strings.update_never),
|
0 to stringResource(MR.strings.update_never),
|
||||||
12 to stringResource(MR.strings.update_12hour),
|
12 to stringResource(MR.strings.update_12hour),
|
||||||
24 to stringResource(MR.strings.update_24hour),
|
24 to stringResource(MR.strings.update_24hour),
|
||||||
@ -169,7 +171,7 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||||||
enabled = autoUpdateInterval > 0,
|
enabled = autoUpdateInterval > 0,
|
||||||
title = stringResource(MR.strings.pref_library_update_restriction),
|
title = stringResource(MR.strings.pref_library_update_restriction),
|
||||||
subtitle = stringResource(MR.strings.restrictions),
|
subtitle = stringResource(MR.strings.restrictions),
|
||||||
entries = mapOf(
|
entries = persistentMapOf(
|
||||||
DEVICE_ONLY_ON_WIFI to stringResource(MR.strings.connected_to_wifi),
|
DEVICE_ONLY_ON_WIFI to stringResource(MR.strings.connected_to_wifi),
|
||||||
DEVICE_NETWORK_NOT_METERED to stringResource(MR.strings.network_not_metered),
|
DEVICE_NETWORK_NOT_METERED to stringResource(MR.strings.network_not_metered),
|
||||||
DEVICE_CHARGING to stringResource(MR.strings.charging),
|
DEVICE_CHARGING to stringResource(MR.strings.charging),
|
||||||
@ -197,7 +199,7 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||||||
Preference.PreferenceItem.MultiSelectListPreference(
|
Preference.PreferenceItem.MultiSelectListPreference(
|
||||||
pref = libraryPreferences.autoUpdateMangaRestrictions(),
|
pref = libraryPreferences.autoUpdateMangaRestrictions(),
|
||||||
title = stringResource(MR.strings.pref_library_update_manga_restriction),
|
title = stringResource(MR.strings.pref_library_update_manga_restriction),
|
||||||
entries = mapOf(
|
entries = persistentMapOf(
|
||||||
MANGA_HAS_UNREAD to stringResource(MR.strings.pref_update_only_completely_read),
|
MANGA_HAS_UNREAD to stringResource(MR.strings.pref_update_only_completely_read),
|
||||||
MANGA_NON_READ to stringResource(MR.strings.pref_update_only_started),
|
MANGA_NON_READ to stringResource(MR.strings.pref_update_only_started),
|
||||||
MANGA_NON_COMPLETED to stringResource(MR.strings.pref_update_only_non_completed),
|
MANGA_NON_COMPLETED to stringResource(MR.strings.pref_update_only_non_completed),
|
||||||
@ -218,11 +220,11 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||||||
): Preference.PreferenceGroup {
|
): Preference.PreferenceGroup {
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_chapter_swipe),
|
title = stringResource(MR.strings.pref_chapter_swipe),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = libraryPreferences.swipeToStartAction(),
|
pref = libraryPreferences.swipeToStartAction(),
|
||||||
title = stringResource(MR.strings.pref_chapter_swipe_start),
|
title = stringResource(MR.strings.pref_chapter_swipe_start),
|
||||||
entries = mapOf(
|
entries = persistentMapOf(
|
||||||
LibraryPreferences.ChapterSwipeAction.Disabled to
|
LibraryPreferences.ChapterSwipeAction.Disabled to
|
||||||
stringResource(MR.strings.disabled),
|
stringResource(MR.strings.disabled),
|
||||||
LibraryPreferences.ChapterSwipeAction.ToggleBookmark to
|
LibraryPreferences.ChapterSwipeAction.ToggleBookmark to
|
||||||
@ -236,7 +238,7 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = libraryPreferences.swipeToEndAction(),
|
pref = libraryPreferences.swipeToEndAction(),
|
||||||
title = stringResource(MR.strings.pref_chapter_swipe_end),
|
title = stringResource(MR.strings.pref_chapter_swipe_end),
|
||||||
entries = mapOf(
|
entries = persistentMapOf(
|
||||||
LibraryPreferences.ChapterSwipeAction.Disabled to
|
LibraryPreferences.ChapterSwipeAction.Disabled to
|
||||||
stringResource(MR.strings.disabled),
|
stringResource(MR.strings.disabled),
|
||||||
LibraryPreferences.ChapterSwipeAction.ToggleBookmark to
|
LibraryPreferences.ChapterSwipeAction.ToggleBookmark to
|
||||||
|
@ -10,6 +10,9 @@ import eu.kanade.presentation.more.settings.Preference
|
|||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableMap
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.util.collectAsState
|
import tachiyomi.presentation.core.util.collectAsState
|
||||||
@ -31,12 +34,13 @@ object SettingsReaderScreen : SearchableSettings {
|
|||||||
pref = readerPref.defaultReadingMode(),
|
pref = readerPref.defaultReadingMode(),
|
||||||
title = stringResource(MR.strings.pref_viewer_type),
|
title = stringResource(MR.strings.pref_viewer_type),
|
||||||
entries = ReadingMode.entries.drop(1)
|
entries = ReadingMode.entries.drop(1)
|
||||||
.associate { it.flagValue to stringResource(it.stringRes) },
|
.associate { it.flagValue to stringResource(it.stringRes) }
|
||||||
|
.toImmutableMap(),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = readerPref.doubleTapAnimSpeed(),
|
pref = readerPref.doubleTapAnimSpeed(),
|
||||||
title = stringResource(MR.strings.pref_double_tap_anim_speed),
|
title = stringResource(MR.strings.pref_double_tap_anim_speed),
|
||||||
entries = mapOf(
|
entries = persistentMapOf(
|
||||||
1 to stringResource(MR.strings.double_tap_anim_speed_0),
|
1 to stringResource(MR.strings.double_tap_anim_speed_0),
|
||||||
500 to stringResource(MR.strings.double_tap_anim_speed_normal),
|
500 to stringResource(MR.strings.double_tap_anim_speed_normal),
|
||||||
250 to stringResource(MR.strings.double_tap_anim_speed_fast),
|
250 to stringResource(MR.strings.double_tap_anim_speed_fast),
|
||||||
@ -82,17 +86,18 @@ object SettingsReaderScreen : SearchableSettings {
|
|||||||
val fullscreen by fullscreenPref.collectAsState()
|
val fullscreen by fullscreenPref.collectAsState()
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_category_display),
|
title = stringResource(MR.strings.pref_category_display),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = readerPreferences.defaultOrientationType(),
|
pref = readerPreferences.defaultOrientationType(),
|
||||||
title = stringResource(MR.strings.pref_rotation_type),
|
title = stringResource(MR.strings.pref_rotation_type),
|
||||||
entries = ReaderOrientation.entries.drop(1)
|
entries = ReaderOrientation.entries.drop(1)
|
||||||
.associate { it.flagValue to stringResource(it.stringRes) },
|
.associate { it.flagValue to stringResource(it.stringRes) }
|
||||||
|
.toImmutableMap(),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = readerPreferences.readerTheme(),
|
pref = readerPreferences.readerTheme(),
|
||||||
title = stringResource(MR.strings.pref_reader_theme),
|
title = stringResource(MR.strings.pref_reader_theme),
|
||||||
entries = mapOf(
|
entries = persistentMapOf(
|
||||||
1 to stringResource(MR.strings.black_background),
|
1 to stringResource(MR.strings.black_background),
|
||||||
2 to stringResource(MR.strings.gray_background),
|
2 to stringResource(MR.strings.gray_background),
|
||||||
0 to stringResource(MR.strings.white_background),
|
0 to stringResource(MR.strings.white_background),
|
||||||
@ -126,7 +131,7 @@ object SettingsReaderScreen : SearchableSettings {
|
|||||||
private fun getReadingGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
|
private fun getReadingGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_category_reading),
|
title = stringResource(MR.strings.pref_category_reading),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
pref = readerPreferences.skipRead(),
|
pref = readerPreferences.skipRead(),
|
||||||
title = stringResource(MR.strings.pref_skip_read_chapters),
|
title = stringResource(MR.strings.pref_skip_read_chapters),
|
||||||
@ -161,23 +166,26 @@ object SettingsReaderScreen : SearchableSettings {
|
|||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pager_viewer),
|
title = stringResource(MR.strings.pager_viewer),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = navModePref,
|
pref = navModePref,
|
||||||
title = stringResource(MR.strings.pref_viewer_nav),
|
title = stringResource(MR.strings.pref_viewer_nav),
|
||||||
entries = ReaderPreferences.TapZones
|
entries = ReaderPreferences.TapZones
|
||||||
.mapIndexed { index, it -> index to stringResource(it) }
|
.mapIndexed { index, it -> index to stringResource(it) }
|
||||||
.toMap(),
|
.toMap()
|
||||||
|
.toImmutableMap(),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = readerPreferences.pagerNavInverted(),
|
pref = readerPreferences.pagerNavInverted(),
|
||||||
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
|
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
|
||||||
entries = listOf(
|
entries = persistentListOf(
|
||||||
ReaderPreferences.TappingInvertMode.NONE,
|
ReaderPreferences.TappingInvertMode.NONE,
|
||||||
ReaderPreferences.TappingInvertMode.HORIZONTAL,
|
ReaderPreferences.TappingInvertMode.HORIZONTAL,
|
||||||
ReaderPreferences.TappingInvertMode.VERTICAL,
|
ReaderPreferences.TappingInvertMode.VERTICAL,
|
||||||
ReaderPreferences.TappingInvertMode.BOTH,
|
ReaderPreferences.TappingInvertMode.BOTH,
|
||||||
).associateWith { stringResource(it.titleRes) },
|
)
|
||||||
|
.associateWith { stringResource(it.titleRes) }
|
||||||
|
.toImmutableMap(),
|
||||||
enabled = navMode != 5,
|
enabled = navMode != 5,
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
@ -185,14 +193,16 @@ object SettingsReaderScreen : SearchableSettings {
|
|||||||
title = stringResource(MR.strings.pref_image_scale_type),
|
title = stringResource(MR.strings.pref_image_scale_type),
|
||||||
entries = ReaderPreferences.ImageScaleType
|
entries = ReaderPreferences.ImageScaleType
|
||||||
.mapIndexed { index, it -> index + 1 to stringResource(it) }
|
.mapIndexed { index, it -> index + 1 to stringResource(it) }
|
||||||
.toMap(),
|
.toMap()
|
||||||
|
.toImmutableMap(),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = readerPreferences.zoomStart(),
|
pref = readerPreferences.zoomStart(),
|
||||||
title = stringResource(MR.strings.pref_zoom_start),
|
title = stringResource(MR.strings.pref_zoom_start),
|
||||||
entries = ReaderPreferences.ZoomStart
|
entries = ReaderPreferences.ZoomStart
|
||||||
.mapIndexed { index, it -> index + 1 to stringResource(it) }
|
.mapIndexed { index, it -> index + 1 to stringResource(it) }
|
||||||
.toMap(),
|
.toMap()
|
||||||
|
.toImmutableMap(),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
pref = readerPreferences.cropBorders(),
|
pref = readerPreferences.cropBorders(),
|
||||||
@ -255,23 +265,26 @@ object SettingsReaderScreen : SearchableSettings {
|
|||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.webtoon_viewer),
|
title = stringResource(MR.strings.webtoon_viewer),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = navModePref,
|
pref = navModePref,
|
||||||
title = stringResource(MR.strings.pref_viewer_nav),
|
title = stringResource(MR.strings.pref_viewer_nav),
|
||||||
entries = ReaderPreferences.TapZones
|
entries = ReaderPreferences.TapZones
|
||||||
.mapIndexed { index, it -> index to stringResource(it) }
|
.mapIndexed { index, it -> index to stringResource(it) }
|
||||||
.toMap(),
|
.toMap()
|
||||||
|
.toImmutableMap(),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = readerPreferences.webtoonNavInverted(),
|
pref = readerPreferences.webtoonNavInverted(),
|
||||||
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
|
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
|
||||||
entries = listOf(
|
entries = persistentListOf(
|
||||||
ReaderPreferences.TappingInvertMode.NONE,
|
ReaderPreferences.TappingInvertMode.NONE,
|
||||||
ReaderPreferences.TappingInvertMode.HORIZONTAL,
|
ReaderPreferences.TappingInvertMode.HORIZONTAL,
|
||||||
ReaderPreferences.TappingInvertMode.VERTICAL,
|
ReaderPreferences.TappingInvertMode.VERTICAL,
|
||||||
ReaderPreferences.TappingInvertMode.BOTH,
|
ReaderPreferences.TappingInvertMode.BOTH,
|
||||||
).associateWith { stringResource(it.titleRes) },
|
)
|
||||||
|
.associateWith { stringResource(it.titleRes) }
|
||||||
|
.toImmutableMap(),
|
||||||
enabled = navMode != 5,
|
enabled = navMode != 5,
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.SliderPreference(
|
Preference.PreferenceItem.SliderPreference(
|
||||||
@ -288,7 +301,7 @@ object SettingsReaderScreen : SearchableSettings {
|
|||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = readerPreferences.readerHideThreshold(),
|
pref = readerPreferences.readerHideThreshold(),
|
||||||
title = stringResource(MR.strings.pref_hide_threshold),
|
title = stringResource(MR.strings.pref_hide_threshold),
|
||||||
entries = mapOf(
|
entries = persistentMapOf(
|
||||||
ReaderPreferences.ReaderHideThreshold.HIGHEST to stringResource(MR.strings.pref_highest),
|
ReaderPreferences.ReaderHideThreshold.HIGHEST to stringResource(MR.strings.pref_highest),
|
||||||
ReaderPreferences.ReaderHideThreshold.HIGH to stringResource(MR.strings.pref_high),
|
ReaderPreferences.ReaderHideThreshold.HIGH to stringResource(MR.strings.pref_high),
|
||||||
ReaderPreferences.ReaderHideThreshold.LOW to stringResource(MR.strings.pref_low),
|
ReaderPreferences.ReaderHideThreshold.LOW to stringResource(MR.strings.pref_low),
|
||||||
@ -341,7 +354,7 @@ object SettingsReaderScreen : SearchableSettings {
|
|||||||
val readWithVolumeKeys by readWithVolumeKeysPref.collectAsState()
|
val readWithVolumeKeys by readWithVolumeKeysPref.collectAsState()
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_reader_navigation),
|
title = stringResource(MR.strings.pref_reader_navigation),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
pref = readWithVolumeKeysPref,
|
pref = readWithVolumeKeysPref,
|
||||||
title = stringResource(MR.strings.pref_read_with_volume_keys),
|
title = stringResource(MR.strings.pref_read_with_volume_keys),
|
||||||
@ -359,7 +372,7 @@ object SettingsReaderScreen : SearchableSettings {
|
|||||||
private fun getActionsGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
|
private fun getActionsGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_reader_actions),
|
title = stringResource(MR.strings.pref_reader_actions),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
pref = readerPreferences.readWithLongTap(),
|
pref = readerPreferences.readWithLongTap(),
|
||||||
title = stringResource(MR.strings.pref_read_with_long_tap),
|
title = stringResource(MR.strings.pref_read_with_long_tap),
|
||||||
|
@ -10,6 +10,8 @@ import eu.kanade.presentation.more.settings.Preference
|
|||||||
import eu.kanade.tachiyomi.core.security.SecurityPreferences
|
import eu.kanade.tachiyomi.core.security.SecurityPreferences
|
||||||
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
|
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
|
||||||
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.isAuthenticationSupported
|
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.isAuthenticationSupported
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableMap
|
||||||
import tachiyomi.core.i18n.stringResource
|
import tachiyomi.core.i18n.stringResource
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.i18n.pluralStringResource
|
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||||
@ -55,7 +57,8 @@ object SettingsSecurityScreen : SearchableSettings {
|
|||||||
0 -> stringResource(MR.strings.lock_always)
|
0 -> stringResource(MR.strings.lock_always)
|
||||||
else -> pluralStringResource(MR.plurals.lock_after_mins, count = it, it)
|
else -> pluralStringResource(MR.plurals.lock_after_mins, count = it, it)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
.toImmutableMap(),
|
||||||
onValueChanged = {
|
onValueChanged = {
|
||||||
(context as FragmentActivity).authenticate(
|
(context as FragmentActivity).authenticate(
|
||||||
title = context.stringResource(MR.strings.lock_when_idle),
|
title = context.stringResource(MR.strings.lock_when_idle),
|
||||||
@ -70,14 +73,15 @@ object SettingsSecurityScreen : SearchableSettings {
|
|||||||
pref = securityPreferences.secureScreen(),
|
pref = securityPreferences.secureScreen(),
|
||||||
title = stringResource(MR.strings.secure_screen),
|
title = stringResource(MR.strings.secure_screen),
|
||||||
entries = SecurityPreferences.SecureScreenMode.entries
|
entries = SecurityPreferences.SecureScreenMode.entries
|
||||||
.associateWith { stringResource(it.titleRes) },
|
.associateWith { stringResource(it.titleRes) }
|
||||||
|
.toImmutableMap(),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.secure_screen_summary)),
|
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.secure_screen_summary)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val LockAfterValues = listOf(
|
private val LockAfterValues = persistentListOf(
|
||||||
0, // Always
|
0, // Always
|
||||||
1,
|
1,
|
||||||
2,
|
2,
|
||||||
|
@ -51,6 +51,8 @@ import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeListApi
|
|||||||
import eu.kanade.tachiyomi.data.track.shikimori.ShikimoriApi
|
import eu.kanade.tachiyomi.data.track.shikimori.ShikimoriApi
|
||||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import tachiyomi.core.util.lang.launchIO
|
import tachiyomi.core.util.lang.launchIO
|
||||||
import tachiyomi.core.util.lang.withUIContext
|
import tachiyomi.core.util.lang.withUIContext
|
||||||
import tachiyomi.domain.source.service.SourceManager
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
@ -125,7 +127,7 @@ object SettingsTrackingScreen : SearchableSettings {
|
|||||||
),
|
),
|
||||||
Preference.PreferenceGroup(
|
Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.services),
|
title = stringResource(MR.strings.services),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.TrackerPreference(
|
Preference.PreferenceItem.TrackerPreference(
|
||||||
title = trackerManager.myAnimeList.name,
|
title = trackerManager.myAnimeList.name,
|
||||||
tracker = trackerManager.myAnimeList,
|
tracker = trackerManager.myAnimeList,
|
||||||
@ -167,7 +169,8 @@ object SettingsTrackingScreen : SearchableSettings {
|
|||||||
),
|
),
|
||||||
Preference.PreferenceGroup(
|
Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.enhanced_services),
|
title = stringResource(MR.strings.enhanced_services),
|
||||||
preferenceItems = enhancedTrackers.first
|
preferenceItems = (
|
||||||
|
enhancedTrackers.first
|
||||||
.map { service ->
|
.map { service ->
|
||||||
Preference.PreferenceItem.TrackerPreference(
|
Preference.PreferenceItem.TrackerPreference(
|
||||||
title = service.name,
|
title = service.name,
|
||||||
@ -175,7 +178,8 @@ object SettingsTrackingScreen : SearchableSettings {
|
|||||||
login = { (service as EnhancedTracker).loginNoop() },
|
login = { (service as EnhancedTracker).loginNoop() },
|
||||||
logout = service::logout,
|
logout = service::logout,
|
||||||
)
|
)
|
||||||
} + listOf(Preference.PreferenceItem.InfoPreference(enhancedTrackerInfo)),
|
} + listOf(Preference.PreferenceItem.InfoPreference(enhancedTrackerInfo))
|
||||||
|
).toImmutableList(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ import android.content.ActivityNotFoundException
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@ -28,16 +27,18 @@ import cafe.adriel.voyager.core.model.rememberScreenModel
|
|||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
|
import eu.kanade.presentation.components.WarningBanner
|
||||||
import eu.kanade.presentation.util.Screen
|
import eu.kanade.presentation.util.Screen
|
||||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags
|
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags
|
||||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
|
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
import eu.kanade.tachiyomi.data.backup.create.BackupCreator
|
||||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import kotlinx.collections.immutable.PersistentSet
|
import kotlinx.collections.immutable.PersistentSet
|
||||||
import kotlinx.collections.immutable.minus
|
import kotlinx.collections.immutable.minus
|
||||||
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
|
import kotlinx.collections.immutable.persistentSetOf
|
||||||
import kotlinx.collections.immutable.plus
|
import kotlinx.collections.immutable.plus
|
||||||
import kotlinx.collections.immutable.toPersistentSet
|
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.LabeledCheckbox
|
import tachiyomi.presentation.core.components.LabeledCheckbox
|
||||||
@ -83,16 +84,21 @@ class CreateBackupScreen : Screen() {
|
|||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier.weight(1f),
|
||||||
.weight(1f)
|
|
||||||
.padding(horizontal = MaterialTheme.padding.medium),
|
|
||||||
) {
|
) {
|
||||||
|
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
|
||||||
|
item {
|
||||||
|
WarningBanner(MR.strings.restore_miui_warning)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
item {
|
item {
|
||||||
LabeledCheckbox(
|
LabeledCheckbox(
|
||||||
label = stringResource(MR.strings.manga),
|
label = stringResource(MR.strings.manga),
|
||||||
checked = true,
|
checked = true,
|
||||||
onCheckedChange = {},
|
onCheckedChange = {},
|
||||||
enabled = false,
|
enabled = false,
|
||||||
|
modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
BackupChoices.forEach { (k, v) ->
|
BackupChoices.forEach { (k, v) ->
|
||||||
@ -103,6 +109,7 @@ class CreateBackupScreen : Screen() {
|
|||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
model.toggleFlag(k)
|
model.toggleFlag(k)
|
||||||
},
|
},
|
||||||
|
modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -116,11 +123,8 @@ class CreateBackupScreen : Screen() {
|
|||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
onClick = {
|
onClick = {
|
||||||
if (!BackupCreateJob.isManualJobRunning(context)) {
|
if (!BackupCreateJob.isManualJobRunning(context)) {
|
||||||
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
|
|
||||||
context.toast(MR.strings.restore_miui_warning, Toast.LENGTH_LONG)
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
chooseBackupDir.launch(Backup.getFilename())
|
chooseBackupDir.launch(BackupCreator.getFilename())
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
context.toast(MR.strings.file_picker_error)
|
context.toast(MR.strings.file_picker_error)
|
||||||
}
|
}
|
||||||
@ -158,11 +162,16 @@ private class CreateBackupScreenModel : StateScreenModel<CreateBackupScreenModel
|
|||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class State(
|
data class State(
|
||||||
val flags: PersistentSet<Int> = BackupChoices.keys.toPersistentSet(),
|
val flags: PersistentSet<Int> = persistentSetOf(
|
||||||
|
BackupCreateFlags.BACKUP_CATEGORY,
|
||||||
|
BackupCreateFlags.BACKUP_CHAPTER,
|
||||||
|
BackupCreateFlags.BACKUP_TRACK,
|
||||||
|
BackupCreateFlags.BACKUP_HISTORY,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val BackupChoices = mapOf(
|
private val BackupChoices = persistentMapOf(
|
||||||
BackupCreateFlags.BACKUP_CATEGORY to MR.strings.categories,
|
BackupCreateFlags.BACKUP_CATEGORY to MR.strings.categories,
|
||||||
BackupCreateFlags.BACKUP_CHAPTER to MR.strings.chapters,
|
BackupCreateFlags.BACKUP_CHAPTER to MR.strings.chapters,
|
||||||
BackupCreateFlags.BACKUP_TRACK to MR.strings.track,
|
BackupCreateFlags.BACKUP_TRACK to MR.strings.track,
|
||||||
|
@ -0,0 +1,242 @@
|
|||||||
|
package eu.kanade.presentation.more.settings.screen.data
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
|
import eu.kanade.presentation.components.AppBar
|
||||||
|
import eu.kanade.presentation.components.WarningBanner
|
||||||
|
import eu.kanade.presentation.util.Screen
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
|
||||||
|
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
|
||||||
|
import eu.kanade.tachiyomi.data.backup.restore.RestoreOptions
|
||||||
|
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||||
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import tachiyomi.core.i18n.stringResource
|
||||||
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
|
||||||
|
class RestoreBackupScreen : Screen() {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
val model = rememberScreenModel { RestoreBackupScreenModel() }
|
||||||
|
val state by model.state.collectAsState()
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
AppBar(
|
||||||
|
title = stringResource(MR.strings.pref_restore_backup),
|
||||||
|
navigateUp = navigator::pop,
|
||||||
|
scrollBehavior = it,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { contentPadding ->
|
||||||
|
if (state.error != null) {
|
||||||
|
val onDismissRequest = model::clearError
|
||||||
|
when (val err = state.error) {
|
||||||
|
is InvalidRestore -> {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
title = { Text(text = stringResource(MR.strings.invalid_backup_file)) },
|
||||||
|
text = { Text(text = listOfNotNull(err.uri, err.message).joinToString("\n\n")) },
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
context.copyToClipboard(err.message, err.message)
|
||||||
|
onDismissRequest()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(MR.strings.action_copy_to_clipboard))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = onDismissRequest) {
|
||||||
|
Text(text = stringResource(MR.strings.action_ok))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is MissingRestoreComponents -> {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
title = { Text(text = stringResource(MR.strings.pref_restore_backup)) },
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||||
|
) {
|
||||||
|
val msg = buildString {
|
||||||
|
append(stringResource(MR.strings.backup_restore_content_full))
|
||||||
|
if (err.sources.isNotEmpty()) {
|
||||||
|
append(
|
||||||
|
"\n\n",
|
||||||
|
).append(stringResource(MR.strings.backup_restore_missing_sources))
|
||||||
|
err.sources.joinTo(
|
||||||
|
this,
|
||||||
|
separator = "\n- ",
|
||||||
|
prefix = "\n- ",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (err.trackers.isNotEmpty()) {
|
||||||
|
append(
|
||||||
|
"\n\n",
|
||||||
|
).append(stringResource(MR.strings.backup_restore_missing_trackers))
|
||||||
|
err.trackers.joinTo(
|
||||||
|
this,
|
||||||
|
separator = "\n- ",
|
||||||
|
prefix = "\n- ",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text(text = msg)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
BackupRestoreJob.start(
|
||||||
|
context = context,
|
||||||
|
uri = err.uri,
|
||||||
|
options = state.options,
|
||||||
|
)
|
||||||
|
onDismissRequest()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(MR.strings.action_restore))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> onDismissRequest() // Unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val chooseBackup = rememberLauncherForActivityResult(
|
||||||
|
object : ActivityResultContracts.GetContent() {
|
||||||
|
override fun createIntent(context: Context, input: String): Intent {
|
||||||
|
val intent = super.createIntent(context, input)
|
||||||
|
return Intent.createChooser(intent, context.stringResource(MR.strings.file_select_backup))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
if (it == null) {
|
||||||
|
context.toast(MR.strings.file_null_uri_error)
|
||||||
|
return@rememberLauncherForActivityResult
|
||||||
|
}
|
||||||
|
|
||||||
|
val results = try {
|
||||||
|
BackupFileValidator(context).validate(it)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
model.setError(InvalidRestore(it, e.message.toString()))
|
||||||
|
return@rememberLauncherForActivityResult
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.missingSources.isEmpty() && results.missingTrackers.isEmpty()) {
|
||||||
|
BackupRestoreJob.start(
|
||||||
|
context = context,
|
||||||
|
uri = it,
|
||||||
|
options = state.options,
|
||||||
|
)
|
||||||
|
return@rememberLauncherForActivityResult
|
||||||
|
}
|
||||||
|
|
||||||
|
model.setError(MissingRestoreComponents(it, results.missingSources, results.missingTrackers))
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(contentPadding)
|
||||||
|
.fillMaxSize(),
|
||||||
|
) {
|
||||||
|
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
|
||||||
|
item {
|
||||||
|
WarningBanner(MR.strings.restore_miui_warning)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Button(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = MaterialTheme.padding.medium)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
onClick = {
|
||||||
|
if (!BackupRestoreJob.isRunning(context)) {
|
||||||
|
// no need to catch because it's wrapped with a chooser
|
||||||
|
chooseBackup.launch("*/*")
|
||||||
|
} else {
|
||||||
|
context.toast(MR.strings.restore_in_progress)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(stringResource(MR.strings.pref_restore_backup))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: show validation errors inline
|
||||||
|
// TODO: show options for what to restore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class RestoreBackupScreenModel : StateScreenModel<RestoreBackupScreenModel.State>(State()) {
|
||||||
|
|
||||||
|
fun setError(error: Any) {
|
||||||
|
mutableState.update {
|
||||||
|
it.copy(error = error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearError() {
|
||||||
|
mutableState.update {
|
||||||
|
it.copy(error = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class State(
|
||||||
|
val error: Any? = null,
|
||||||
|
// TODO: allow user-selectable restore options
|
||||||
|
val options: RestoreOptions = RestoreOptions(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class MissingRestoreComponents(
|
||||||
|
val uri: Uri,
|
||||||
|
val sources: List<String>,
|
||||||
|
val trackers: List<String>,
|
||||||
|
)
|
||||||
|
|
||||||
|
private data class InvalidRestore(
|
||||||
|
val uri: Uri? = null,
|
||||||
|
val message: String,
|
||||||
|
)
|
@ -0,0 +1,74 @@
|
|||||||
|
package eu.kanade.presentation.more.settings.screen.data
|
||||||
|
|
||||||
|
import android.text.format.Formatter
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
import tachiyomi.presentation.core.theme.header
|
||||||
|
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StorageInfo(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val storages = remember { DiskUtil.getExternalStorages(context) }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
storages.forEach {
|
||||||
|
StorageInfo(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun StorageInfo(
|
||||||
|
file: File,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val available = remember(file) { DiskUtil.getAvailableStorageSpace(file) }
|
||||||
|
val availableText = remember(available) { Formatter.formatFileSize(context, available) }
|
||||||
|
val total = remember(file) { DiskUtil.getTotalStorageSpace(file) }
|
||||||
|
val totalText = remember(total) { Formatter.formatFileSize(context, total) }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = file.absolutePath,
|
||||||
|
style = MaterialTheme.typography.header,
|
||||||
|
)
|
||||||
|
|
||||||
|
LinearProgressIndicator(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(MaterialTheme.shapes.small)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(12.dp),
|
||||||
|
progress = { (1 - (available / total.toFloat())) },
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(MR.strings.available_disk_space_info, availableText, totalText),
|
||||||
|
modifier = Modifier.secondaryItemAlpha(),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -15,6 +15,8 @@ import eu.kanade.presentation.more.settings.screen.about.AboutScreen
|
|||||||
import eu.kanade.presentation.util.Screen
|
import eu.kanade.presentation.util.Screen
|
||||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||||
|
import kotlinx.collections.immutable.mutate
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.guava.await
|
import kotlinx.coroutines.guava.await
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
|
|
||||||
@ -47,7 +49,7 @@ class DebugInfoScreen : Screen() {
|
|||||||
private fun getAppInfoGroup(): Preference.PreferenceGroup {
|
private fun getAppInfoGroup(): Preference.PreferenceGroup {
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = "App info",
|
title = "App info",
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = "Version",
|
title = "Version",
|
||||||
subtitle = AboutScreen.getVersionName(false),
|
subtitle = AboutScreen.getVersionName(false),
|
||||||
@ -96,8 +98,8 @@ class DebugInfoScreen : Screen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getDeviceInfoGroup(): Preference.PreferenceGroup {
|
private fun getDeviceInfoGroup(): Preference.PreferenceGroup {
|
||||||
val items = buildList {
|
val items = persistentListOf<Preference.PreferenceItem<out Any>>().mutate {
|
||||||
add(
|
it.add(
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = "Model",
|
title = "Model",
|
||||||
subtitle = "${Build.MANUFACTURER} ${Build.MODEL} (${Build.DEVICE})",
|
subtitle = "${Build.MANUFACTURER} ${Build.MODEL} (${Build.DEVICE})",
|
||||||
@ -105,14 +107,14 @@ class DebugInfoScreen : Screen() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (DeviceUtil.oneUiVersion != null) {
|
if (DeviceUtil.oneUiVersion != null) {
|
||||||
add(
|
it.add(
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = "OneUI version",
|
title = "OneUI version",
|
||||||
subtitle = "${DeviceUtil.oneUiVersion}",
|
subtitle = "${DeviceUtil.oneUiVersion}",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
} else if (DeviceUtil.miuiMajorVersion != null) {
|
} else if (DeviceUtil.miuiMajorVersion != null) {
|
||||||
add(
|
it.add(
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = "MIUI version",
|
title = "MIUI version",
|
||||||
subtitle = "${DeviceUtil.miuiMajorVersion}",
|
subtitle = "${DeviceUtil.miuiMajorVersion}",
|
||||||
@ -127,7 +129,7 @@ class DebugInfoScreen : Screen() {
|
|||||||
} else {
|
} else {
|
||||||
Build.VERSION.RELEASE
|
Build.VERSION.RELEASE
|
||||||
}
|
}
|
||||||
add(
|
it.add(
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = "Android version",
|
title = "Android version",
|
||||||
subtitle = "$androidVersion (${Build.DISPLAY})",
|
subtitle = "$androidVersion (${Build.DISPLAY})",
|
||||||
|
@ -114,6 +114,7 @@ internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier = comp
|
|||||||
} else {
|
} else {
|
||||||
tween(200)
|
tween(200)
|
||||||
},
|
},
|
||||||
|
label = "highlight",
|
||||||
)
|
)
|
||||||
Modifier.background(color = highlight)
|
Modifier.background(color = highlight)
|
||||||
}
|
}
|
||||||
|
@ -140,7 +140,7 @@ private fun TrackerStats(
|
|||||||
val meanScoreStr = remember(data.trackedTitleCount, data.meanScore) {
|
val meanScoreStr = remember(data.trackedTitleCount, data.meanScore) {
|
||||||
if (data.trackedTitleCount > 0 && !data.meanScore.isNaN()) {
|
if (data.trackedTitleCount > 0 && !data.meanScore.isNaN()) {
|
||||||
// All other numbers are localized in English
|
// All other numbers are localized in English
|
||||||
String.format(Locale.ENGLISH, "%.2f ★", data.meanScore)
|
"%.2f ★".format(Locale.ENGLISH, data.meanScore)
|
||||||
} else {
|
} else {
|
||||||
notApplicable
|
notApplicable
|
||||||
}
|
}
|
||||||
|
@ -34,11 +34,11 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.toDomainChapter
|
import eu.kanade.tachiyomi.data.database.models.toDomainChapter
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||||
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
import tachiyomi.domain.chapter.service.calculateChapterGap
|
import tachiyomi.domain.chapter.service.calculateChapterGap
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.i18n.pluralStringResource
|
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||||
@ -51,8 +51,8 @@ fun ChapterTransition(
|
|||||||
currChapterDownloaded: Boolean,
|
currChapterDownloaded: Boolean,
|
||||||
goingToChapterDownloaded: Boolean,
|
goingToChapterDownloaded: Boolean,
|
||||||
) {
|
) {
|
||||||
val currChapter = transition.from.chapter
|
val currChapter = transition.from.chapter.toDomainChapter()
|
||||||
val goingToChapter = transition.to?.chapter
|
val goingToChapter = transition.to?.chapter?.toDomainChapter()
|
||||||
|
|
||||||
ProvideTextStyle(MaterialTheme.typography.bodyMedium) {
|
ProvideTextStyle(MaterialTheme.typography.bodyMedium) {
|
||||||
when (transition) {
|
when (transition) {
|
||||||
@ -65,7 +65,7 @@ fun ChapterTransition(
|
|||||||
bottomChapter = currChapter,
|
bottomChapter = currChapter,
|
||||||
bottomChapterDownloaded = currChapterDownloaded,
|
bottomChapterDownloaded = currChapterDownloaded,
|
||||||
fallbackLabel = stringResource(MR.strings.transition_no_previous),
|
fallbackLabel = stringResource(MR.strings.transition_no_previous),
|
||||||
chapterGap = calculateChapterGap(currChapter.toDomainChapter(), goingToChapter?.toDomainChapter()),
|
chapterGap = calculateChapterGap(currChapter, goingToChapter),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is ChapterTransition.Next -> {
|
is ChapterTransition.Next -> {
|
||||||
@ -77,7 +77,7 @@ fun ChapterTransition(
|
|||||||
bottomChapter = goingToChapter,
|
bottomChapter = goingToChapter,
|
||||||
bottomChapterDownloaded = goingToChapterDownloaded,
|
bottomChapterDownloaded = goingToChapterDownloaded,
|
||||||
fallbackLabel = stringResource(MR.strings.transition_no_next),
|
fallbackLabel = stringResource(MR.strings.transition_no_next),
|
||||||
chapterGap = calculateChapterGap(goingToChapter?.toDomainChapter(), currChapter.toDomainChapter()),
|
chapterGap = calculateChapterGap(goingToChapter, currChapter),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -235,7 +235,7 @@ private fun ChapterText(
|
|||||||
maxLines = 5,
|
maxLines = 5,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
inlineContent = mapOf(
|
inlineContent = persistentMapOf(
|
||||||
DownloadedIconContentId to InlineTextContent(
|
DownloadedIconContentId to InlineTextContent(
|
||||||
Placeholder(
|
Placeholder(
|
||||||
width = 22.sp,
|
width = 22.sp,
|
||||||
@ -275,24 +275,23 @@ private val CardColor: CardColors
|
|||||||
private val VerticalSpacerSize = 24.dp
|
private val VerticalSpacerSize = 24.dp
|
||||||
private const val DownloadedIconContentId = "downloaded"
|
private const val DownloadedIconContentId = "downloaded"
|
||||||
|
|
||||||
private fun previewChapter(name: String, scanlator: String, chapterNumber: Float) = ChapterImpl().apply {
|
private fun previewChapter(name: String, scanlator: String, chapterNumber: Double) = Chapter.create().copy(
|
||||||
this.name = name
|
id = 0L,
|
||||||
this.scanlator = scanlator
|
mangaId = 0L,
|
||||||
this.chapter_number = chapterNumber
|
url = "",
|
||||||
|
name = name,
|
||||||
this.id = 0
|
scanlator = scanlator,
|
||||||
this.manga_id = 0
|
chapterNumber = chapterNumber,
|
||||||
this.url = ""
|
)
|
||||||
}
|
|
||||||
private val FakeChapter = previewChapter(
|
private val FakeChapter = previewChapter(
|
||||||
name = "Vol.1, Ch.1 - Fake Chapter Title",
|
name = "Vol.1, Ch.1 - Fake Chapter Title",
|
||||||
scanlator = "Scanlator Name",
|
scanlator = "Scanlator Name",
|
||||||
chapterNumber = 1f,
|
chapterNumber = 1.0,
|
||||||
)
|
)
|
||||||
private val FakeGapChapter = previewChapter(
|
private val FakeGapChapter = previewChapter(
|
||||||
name = "Vol.5, Ch.44 - Fake Gap Chapter Title",
|
name = "Vol.5, Ch.44 - Fake Gap Chapter Title",
|
||||||
scanlator = "Scanlator Name",
|
scanlator = "Scanlator Name",
|
||||||
chapterNumber = 44f,
|
chapterNumber = 44.0,
|
||||||
)
|
)
|
||||||
private val FakeChapterLongTitle = previewChapter(
|
private val FakeChapterLongTitle = previewChapter(
|
||||||
name = "Vol.1, Ch.0 - The Mundane Musings of a Metafictional Manga: A Chapter About a Chapter, Featuring" +
|
name = "Vol.1, Ch.0 - The Mundane Musings of a Metafictional Manga: A Chapter About a Chapter, Featuring" +
|
||||||
@ -301,7 +300,7 @@ private val FakeChapterLongTitle = previewChapter(
|
|||||||
"Fictional Realities and Reality-Bending Fiction, Where the Fourth Wall is Always in Danger of Being Broken " +
|
"Fictional Realities and Reality-Bending Fiction, Where the Fourth Wall is Always in Danger of Being Broken " +
|
||||||
"and the Line Between Author and Character is Forever Blurred.",
|
"and the Line Between Author and Character is Forever Blurred.",
|
||||||
scanlator = "Long Long Funny Scanlator Sniper Group Name Reborn",
|
scanlator = "Long Long Funny Scanlator Sniper Group Name Reborn",
|
||||||
chapterNumber = 1f,
|
chapterNumber = 1.0,
|
||||||
)
|
)
|
||||||
|
|
||||||
@PreviewLightDark
|
@PreviewLightDark
|
||||||
|
@ -30,7 +30,7 @@ fun DisplayRefreshHost(
|
|||||||
val currentDisplayRefresh = hostState.currentDisplayRefresh
|
val currentDisplayRefresh = hostState.currentDisplayRefresh
|
||||||
LaunchedEffect(currentDisplayRefresh) {
|
LaunchedEffect(currentDisplayRefresh) {
|
||||||
if (currentDisplayRefresh) {
|
if (currentDisplayRefresh) {
|
||||||
delay(200)
|
delay(1500)
|
||||||
hostState.currentDisplayRefresh = false
|
hostState.currentDisplayRefresh = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -39,7 +39,7 @@ fun DisplayRefreshHost(
|
|||||||
Canvas(
|
Canvas(
|
||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
drawRect(Color.White)
|
drawRect(Color.Black)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,6 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark
|
|||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import eu.kanade.domain.track.model.toDbTrack
|
|
||||||
import eu.kanade.presentation.components.DropdownMenu
|
import eu.kanade.presentation.components.DropdownMenu
|
||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||||
import eu.kanade.presentation.track.components.TrackLogoIcon
|
import eu.kanade.presentation.track.components.TrackLogoIcon
|
||||||
@ -101,7 +100,7 @@ fun TrackInfoDialogHome(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onChaptersClick = { onChapterClick(item) },
|
onChaptersClick = { onChapterClick(item) },
|
||||||
score = item.tracker.displayScore(item.track.toDbTrack())
|
score = item.tracker.displayScore(item.track)
|
||||||
.takeIf { supportsScoring && item.track.score != 0.0 },
|
.takeIf { supportsScoring && item.track.score != 0.0 },
|
||||||
onScoreClick = { onScoreClick(item) }
|
onScoreClick = { onScoreClick(item) }
|
||||||
.takeIf { supportsScoring },
|
.takeIf { supportsScoring },
|
||||||
|
@ -13,7 +13,7 @@ internal class TrackInfoDialogHomePreviewProvider :
|
|||||||
private val aTrack = Track(
|
private val aTrack = Track(
|
||||||
id = 1L,
|
id = 1L,
|
||||||
mangaId = 2L,
|
mangaId = 2L,
|
||||||
syncId = 3L,
|
trackerId = 3L,
|
||||||
remoteId = 4L,
|
remoteId = 4L,
|
||||||
libraryId = null,
|
libraryId = null,
|
||||||
title = "Manage Name On Tracker Site",
|
title = "Manage Name On Tracker Site",
|
||||||
|
@ -34,6 +34,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
||||||
@ -233,7 +234,7 @@ private fun TrackStatusSelectorPreviews() {
|
|||||||
TrackStatusSelector(
|
TrackStatusSelector(
|
||||||
selection = 1,
|
selection = 1,
|
||||||
onSelectionChange = {},
|
onSelectionChange = {},
|
||||||
selections = mapOf(
|
selections = persistentMapOf(
|
||||||
// Anilist values
|
// Anilist values
|
||||||
1 to MR.strings.reading,
|
1 to MR.strings.reading,
|
||||||
2 to MR.strings.plan_to_read,
|
2 to MR.strings.plan_to_read,
|
||||||
|
@ -62,8 +62,8 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
|
|||||||
private fun randTrackSearch() = TrackSearch().let {
|
private fun randTrackSearch() = TrackSearch().let {
|
||||||
it.id = Random.nextLong()
|
it.id = Random.nextLong()
|
||||||
it.manga_id = Random.nextLong()
|
it.manga_id = Random.nextLong()
|
||||||
it.sync_id = Random.nextInt()
|
it.tracker_id = Random.nextInt()
|
||||||
it.media_id = Random.nextLong()
|
it.remote_id = Random.nextLong()
|
||||||
it.library_id = Random.nextLong()
|
it.library_id = Random.nextLong()
|
||||||
it.title = lorem((1..10).random()).joinToString()
|
it.title = lorem((1..10).random()).joinToString()
|
||||||
it.last_chapter_read = (0..100).random().toFloat()
|
it.last_chapter_read = (0..100).random().toFloat()
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
|
||||||
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
|
import okio.buffer
|
||||||
|
import okio.gzip
|
||||||
|
import okio.source
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class BackupDecoder(
|
||||||
|
private val context: Context,
|
||||||
|
private val parser: ProtoBuf = Injekt.get(),
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a potentially-gzipped backup.
|
||||||
|
*/
|
||||||
|
fun decode(uri: Uri): Backup {
|
||||||
|
return context.contentResolver.openInputStream(uri)!!.use { inputStream ->
|
||||||
|
val source = inputStream.source().buffer()
|
||||||
|
|
||||||
|
val peeked = source.peek().apply {
|
||||||
|
require(2)
|
||||||
|
}
|
||||||
|
val id1id2 = peeked.readShort()
|
||||||
|
val backupString = if (id1id2.toInt() == 0x1f8b) { // 0x1f8b is gzip magic bytes
|
||||||
|
source.gzip().buffer()
|
||||||
|
} else {
|
||||||
|
source
|
||||||
|
}.use { it.readByteArray() }
|
||||||
|
|
||||||
|
parser.decodeFromByteArray(BackupSerializer, backupString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.data.backup
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.data.track.TrackerManager
|
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||||
import eu.kanade.tachiyomi.util.BackupUtil
|
|
||||||
import tachiyomi.core.i18n.stringResource
|
import tachiyomi.core.i18n.stringResource
|
||||||
import tachiyomi.domain.source.service.SourceManager
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
@ -11,6 +10,8 @@ import uy.kohesive.injekt.Injekt
|
|||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class BackupFileValidator(
|
class BackupFileValidator(
|
||||||
|
private val context: Context,
|
||||||
|
|
||||||
private val sourceManager: SourceManager = Injekt.get(),
|
private val sourceManager: SourceManager = Injekt.get(),
|
||||||
private val trackerManager: TrackerManager = Injekt.get(),
|
private val trackerManager: TrackerManager = Injekt.get(),
|
||||||
) {
|
) {
|
||||||
@ -21,9 +22,9 @@ class BackupFileValidator(
|
|||||||
* @throws Exception if manga cannot be found.
|
* @throws Exception if manga cannot be found.
|
||||||
* @return List of missing sources or missing trackers.
|
* @return List of missing sources or missing trackers.
|
||||||
*/
|
*/
|
||||||
fun validate(context: Context, uri: Uri): Results {
|
fun validate(uri: Uri): Results {
|
||||||
val backup = try {
|
val backup = try {
|
||||||
BackupUtil.decodeBackup(context, uri)
|
BackupDecoder(context).decode(uri)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw IllegalStateException(e)
|
throw IllegalStateException(e)
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,6 @@ import tachiyomi.domain.backup.service.BackupPreferences
|
|||||||
import tachiyomi.domain.storage.service.StorageManager
|
import tachiyomi.domain.storage.service.StorageManager
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.time.Instant
|
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class BackupCreateJob(private val context: Context, workerParams: WorkerParameters) :
|
class BackupCreateJob(private val context: Context, workerParams: WorkerParameters) :
|
||||||
@ -49,13 +48,10 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
|
|||||||
setForegroundSafely()
|
setForegroundSafely()
|
||||||
|
|
||||||
val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupCreateFlags.AutomaticDefaults)
|
val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupCreateFlags.AutomaticDefaults)
|
||||||
val backupPreferences = Injekt.get<BackupPreferences>()
|
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
val location = BackupCreator(context).createBackup(uri, flags, isAutoBackup)
|
val location = BackupCreator(context, isAutoBackup).backup(uri, flags)
|
||||||
if (isAutoBackup) {
|
if (!isAutoBackup) {
|
||||||
backupPreferences.lastAutoBackupTimestamp().set(Instant.now().toEpochMilli())
|
|
||||||
} else {
|
|
||||||
notifier.showBackupComplete(UniFile.fromUri(context, location.toUri())!!)
|
notifier.showBackupComplete(UniFile.fromUri(context, location.toUri())!!)
|
||||||
}
|
}
|
||||||
Result.success()
|
Result.success()
|
||||||
|
@ -3,85 +3,56 @@ package eu.kanade.tachiyomi.data.backup.create
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
|
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
|
||||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_APP_PREFS
|
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_APP_PREFS
|
||||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_CATEGORY
|
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_CATEGORY
|
||||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_CHAPTER
|
|
||||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_HISTORY
|
|
||||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_SOURCE_PREFS
|
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_SOURCE_PREFS
|
||||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_TRACK
|
import eu.kanade.tachiyomi.data.backup.create.creators.CategoriesBackupCreator
|
||||||
|
import eu.kanade.tachiyomi.data.backup.create.creators.MangaBackupCreator
|
||||||
|
import eu.kanade.tachiyomi.data.backup.create.creators.PreferenceBackupCreator
|
||||||
|
import eu.kanade.tachiyomi.data.backup.create.creators.SourcesBackupCreator
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupChapter
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
|
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
|
import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupSource
|
import eu.kanade.tachiyomi.data.backup.models.BackupSource
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
|
import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BooleanPreferenceValue
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.FloatPreferenceValue
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.IntPreferenceValue
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.LongPreferenceValue
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.StringPreferenceValue
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.StringSetPreferenceValue
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.backupCategoryMapper
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.backupChapterMapper
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.backupTrackMapper
|
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
|
||||||
import eu.kanade.tachiyomi.source.preferenceKey
|
|
||||||
import eu.kanade.tachiyomi.source.sourcePreferences
|
|
||||||
import kotlinx.serialization.protobuf.ProtoBuf
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.gzip
|
import okio.gzip
|
||||||
import okio.sink
|
import okio.sink
|
||||||
import tachiyomi.core.i18n.stringResource
|
import tachiyomi.core.i18n.stringResource
|
||||||
import tachiyomi.core.preference.Preference
|
|
||||||
import tachiyomi.core.preference.PreferenceStore
|
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
import tachiyomi.data.DatabaseHandler
|
import tachiyomi.domain.backup.service.BackupPreferences
|
||||||
import tachiyomi.domain.category.interactor.GetCategories
|
|
||||||
import tachiyomi.domain.category.model.Category
|
|
||||||
import tachiyomi.domain.history.interactor.GetHistory
|
|
||||||
import tachiyomi.domain.manga.interactor.GetFavorites
|
import tachiyomi.domain.manga.interactor.GetFavorites
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.source.service.SourceManager
|
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
class BackupCreator(
|
class BackupCreator(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
|
private val isAutoBackup: Boolean,
|
||||||
|
|
||||||
|
private val parser: ProtoBuf = Injekt.get(),
|
||||||
|
private val getFavorites: GetFavorites = Injekt.get(),
|
||||||
|
private val backupPreferences: BackupPreferences = Injekt.get(),
|
||||||
|
|
||||||
|
private val categoriesBackupCreator: CategoriesBackupCreator = CategoriesBackupCreator(),
|
||||||
|
private val mangaBackupCreator: MangaBackupCreator = MangaBackupCreator(),
|
||||||
|
private val preferenceBackupCreator: PreferenceBackupCreator = PreferenceBackupCreator(),
|
||||||
|
private val sourcesBackupCreator: SourcesBackupCreator = SourcesBackupCreator(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val handler: DatabaseHandler = Injekt.get()
|
suspend fun backup(uri: Uri, flags: Int): String {
|
||||||
private val sourceManager: SourceManager = Injekt.get()
|
|
||||||
private val getCategories: GetCategories = Injekt.get()
|
|
||||||
private val getFavorites: GetFavorites = Injekt.get()
|
|
||||||
private val getHistory: GetHistory = Injekt.get()
|
|
||||||
private val preferenceStore: PreferenceStore = Injekt.get()
|
|
||||||
|
|
||||||
internal val parser = ProtoBuf
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create backup file.
|
|
||||||
*
|
|
||||||
* @param uri path of Uri
|
|
||||||
* @param isAutoBackup backup called from scheduled backup job
|
|
||||||
*/
|
|
||||||
suspend fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String {
|
|
||||||
val databaseManga = getFavorites.await()
|
|
||||||
val backup = Backup(
|
|
||||||
backupMangas(databaseManga, flags),
|
|
||||||
backupCategories(flags),
|
|
||||||
emptyList(),
|
|
||||||
prepExtensionInfoForSync(databaseManga),
|
|
||||||
backupAppPreferences(flags),
|
|
||||||
backupSourcePreferences(flags),
|
|
||||||
)
|
|
||||||
|
|
||||||
var file: UniFile? = null
|
var file: UniFile? = null
|
||||||
try {
|
try {
|
||||||
file = (
|
file = (
|
||||||
@ -90,14 +61,14 @@ class BackupCreator(
|
|||||||
val dir = UniFile.fromUri(context, uri)
|
val dir = UniFile.fromUri(context, uri)
|
||||||
|
|
||||||
// Delete older backups
|
// Delete older backups
|
||||||
dir?.listFiles { _, filename -> Backup.filenameRegex.matches(filename) }
|
dir?.listFiles { _, filename -> FILENAME_REGEX.matches(filename) }
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
.sortedByDescending { it.name }
|
.sortedByDescending { it.name }
|
||||||
.drop(MAX_AUTO_BACKUPS - 1)
|
.drop(MAX_AUTO_BACKUPS - 1)
|
||||||
.forEach { it.delete() }
|
.forEach { it.delete() }
|
||||||
|
|
||||||
// Create new file to place backup
|
// Create new file to place backup
|
||||||
dir?.createFile(Backup.getFilename())
|
dir?.createFile(getFilename())
|
||||||
} else {
|
} else {
|
||||||
UniFile.fromUri(context, uri)
|
UniFile.fromUri(context, uri)
|
||||||
}
|
}
|
||||||
@ -108,19 +79,36 @@ class BackupCreator(
|
|||||||
throw IllegalStateException("Failed to get handle on a backup file")
|
throw IllegalStateException("Failed to get handle on a backup file")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val databaseManga = getFavorites.await()
|
||||||
|
val backup = Backup(
|
||||||
|
backupManga = backupMangas(databaseManga, flags),
|
||||||
|
backupCategories = backupCategories(flags),
|
||||||
|
backupSources = backupSources(databaseManga),
|
||||||
|
backupPreferences = backupAppPreferences(flags),
|
||||||
|
backupSourcePreferences = backupSourcePreferences(flags),
|
||||||
|
)
|
||||||
|
|
||||||
val byteArray = parser.encodeToByteArray(BackupSerializer, backup)
|
val byteArray = parser.encodeToByteArray(BackupSerializer, backup)
|
||||||
if (byteArray.isEmpty()) {
|
if (byteArray.isEmpty()) {
|
||||||
throw IllegalStateException(context.stringResource(MR.strings.empty_backup_error))
|
throw IllegalStateException(context.stringResource(MR.strings.empty_backup_error))
|
||||||
}
|
}
|
||||||
|
|
||||||
file.openOutputStream().also {
|
file.openOutputStream()
|
||||||
|
.also {
|
||||||
// Force overwrite old file
|
// Force overwrite old file
|
||||||
(it as? FileOutputStream)?.channel?.truncate(0)
|
(it as? FileOutputStream)?.channel?.truncate(0)
|
||||||
}.sink().gzip().buffer().use { it.write(byteArray) }
|
}
|
||||||
|
.sink().gzip().buffer().use {
|
||||||
|
it.write(byteArray)
|
||||||
|
}
|
||||||
val fileUri = file.uri
|
val fileUri = file.uri
|
||||||
|
|
||||||
// Make sure it's a valid backup file
|
// Make sure it's a valid backup file
|
||||||
BackupFileValidator().validate(context, fileUri)
|
BackupFileValidator(context).validate(fileUri)
|
||||||
|
|
||||||
|
if (isAutoBackup) {
|
||||||
|
backupPreferences.lastAutoBackupTimestamp().set(Instant.now().toEpochMilli())
|
||||||
|
}
|
||||||
|
|
||||||
return fileUri.toString()
|
return fileUri.toString()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@ -130,135 +118,39 @@ class BackupCreator(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun prepExtensionInfoForSync(mangas: List<Manga>): List<BackupSource> {
|
private suspend fun backupCategories(options: Int): List<BackupCategory> {
|
||||||
return mangas
|
if (options and BACKUP_CATEGORY != BACKUP_CATEGORY) return emptyList()
|
||||||
.asSequence()
|
|
||||||
.map(Manga::source)
|
|
||||||
.distinct()
|
|
||||||
.map(sourceManager::getOrStub)
|
|
||||||
.map(BackupSource::copyFrom)
|
|
||||||
.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
return categoriesBackupCreator.backupCategories()
|
||||||
* Backup the categories of library
|
|
||||||
*
|
|
||||||
* @return list of [BackupCategory] to be backed up
|
|
||||||
*/
|
|
||||||
suspend fun backupCategories(options: Int): List<BackupCategory> {
|
|
||||||
// Check if user wants category information in backup
|
|
||||||
return if (options and BACKUP_CATEGORY == BACKUP_CATEGORY) {
|
|
||||||
getCategories.await()
|
|
||||||
.filterNot(Category::isSystemCategory)
|
|
||||||
.map(backupCategoryMapper)
|
|
||||||
} else {
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun backupMangas(mangas: List<Manga>, flags: Int): List<BackupManga> {
|
suspend fun backupMangas(mangas: List<Manga>, flags: Int): List<BackupManga> {
|
||||||
return mangas.map {
|
return mangaBackupCreator.backupMangas(mangas, flags)
|
||||||
backupManga(it, flags)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private fun backupSources(mangas: List<Manga>): List<BackupSource> {
|
||||||
* Convert a manga to Json
|
return sourcesBackupCreator.backupSources(mangas)
|
||||||
*
|
|
||||||
* @param manga manga that gets converted
|
|
||||||
* @param options options for the backup
|
|
||||||
* @return [BackupManga] containing manga in a serializable form
|
|
||||||
*/
|
|
||||||
suspend fun backupManga(manga: Manga, options: Int): BackupManga {
|
|
||||||
// Entry for this manga
|
|
||||||
val mangaObject = BackupManga.copyFrom(manga)
|
|
||||||
|
|
||||||
// Check if user wants chapter information in backup
|
|
||||||
if (options and BACKUP_CHAPTER == BACKUP_CHAPTER) {
|
|
||||||
// Backup all the chapters
|
|
||||||
handler.awaitList {
|
|
||||||
chaptersQueries.getChaptersByMangaId(
|
|
||||||
mangaId = manga.id,
|
|
||||||
applyScanlatorFilter = 0, // false
|
|
||||||
mapper = backupChapterMapper,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.takeUnless(List<BackupChapter>::isEmpty)
|
|
||||||
?.let { mangaObject.chapters = it }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user wants category information in backup
|
|
||||||
if (options and BACKUP_CATEGORY == BACKUP_CATEGORY) {
|
|
||||||
// Backup categories for this manga
|
|
||||||
val categoriesForManga = getCategories.await(manga.id)
|
|
||||||
if (categoriesForManga.isNotEmpty()) {
|
|
||||||
mangaObject.categories = categoriesForManga.map { it.order }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user wants track information in backup
|
|
||||||
if (options and BACKUP_TRACK == BACKUP_TRACK) {
|
|
||||||
val tracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id, backupTrackMapper) }
|
|
||||||
if (tracks.isNotEmpty()) {
|
|
||||||
mangaObject.tracking = tracks
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user wants history information in backup
|
|
||||||
if (options and BACKUP_HISTORY == BACKUP_HISTORY) {
|
|
||||||
val historyByMangaId = getHistory.await(manga.id)
|
|
||||||
if (historyByMangaId.isNotEmpty()) {
|
|
||||||
val history = historyByMangaId.map { history ->
|
|
||||||
val chapter = handler.awaitOne { chaptersQueries.getChapterById(history.chapterId) }
|
|
||||||
BackupHistory(chapter.url, history.readAt?.time ?: 0L, history.readDuration)
|
|
||||||
}
|
|
||||||
if (history.isNotEmpty()) {
|
|
||||||
mangaObject.history = history
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return mangaObject
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun backupAppPreferences(flags: Int): List<BackupPreference> {
|
fun backupAppPreferences(flags: Int): List<BackupPreference> {
|
||||||
if (flags and BACKUP_APP_PREFS != BACKUP_APP_PREFS) return emptyList()
|
if (flags and BACKUP_APP_PREFS != BACKUP_APP_PREFS) return emptyList()
|
||||||
|
|
||||||
return preferenceStore.getAll().toBackupPreferences()
|
return preferenceBackupCreator.backupAppPreferences()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun backupSourcePreferences(flags: Int): List<BackupSourcePreferences> {
|
fun backupSourcePreferences(flags: Int): List<BackupSourcePreferences> {
|
||||||
if (flags and BACKUP_SOURCE_PREFS != BACKUP_SOURCE_PREFS) return emptyList()
|
if (flags and BACKUP_SOURCE_PREFS != BACKUP_SOURCE_PREFS) return emptyList()
|
||||||
|
|
||||||
return sourceManager.getCatalogueSources()
|
return preferenceBackupCreator.backupSourcePreferences()
|
||||||
.filterIsInstance<ConfigurableSource>()
|
|
||||||
.map {
|
|
||||||
BackupSourcePreferences(
|
|
||||||
it.preferenceKey(),
|
|
||||||
it.sourcePreferences().all.toBackupPreferences(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
companion object {
|
||||||
private fun Map<String, *>.toBackupPreferences(): List<BackupPreference> {
|
private const val MAX_AUTO_BACKUPS: Int = 4
|
||||||
return this.filterKeys {
|
private val FILENAME_REGEX = """${BuildConfig.APPLICATION_ID}_\d{4}-\d{2}-\d{2}_\d{2}-\d{2}.tachibk""".toRegex()
|
||||||
!Preference.isPrivate(it) && !Preference.isAppState(it)
|
|
||||||
}
|
fun getFilename(): String {
|
||||||
.mapNotNull { (key, value) ->
|
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.ENGLISH).format(Date())
|
||||||
when (value) {
|
return "${BuildConfig.APPLICATION_ID}_$date.tachibk"
|
||||||
is Int -> BackupPreference(key, IntPreferenceValue(value))
|
|
||||||
is Long -> BackupPreference(key, LongPreferenceValue(value))
|
|
||||||
is Float -> BackupPreference(key, FloatPreferenceValue(value))
|
|
||||||
is String -> BackupPreference(key, StringPreferenceValue(value))
|
|
||||||
is Boolean -> BackupPreference(key, BooleanPreferenceValue(value))
|
|
||||||
is Set<*> -> (value as? Set<String>)?.let {
|
|
||||||
BackupPreference(key, StringSetPreferenceValue(it))
|
|
||||||
}
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val MAX_AUTO_BACKUPS: Int = 4
|
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.create.creators
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.backupCategoryMapper
|
||||||
|
import tachiyomi.domain.category.interactor.GetCategories
|
||||||
|
import tachiyomi.domain.category.model.Category
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class CategoriesBackupCreator(
|
||||||
|
private val getCategories: GetCategories = Injekt.get(),
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun backupCategories(): List<BackupCategory> {
|
||||||
|
return getCategories.await()
|
||||||
|
.filterNot(Category::isSystemCategory)
|
||||||
|
.map(backupCategoryMapper)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,101 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.create.creators
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BackupChapter
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.backupChapterMapper
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.backupTrackMapper
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
||||||
|
import tachiyomi.data.DatabaseHandler
|
||||||
|
import tachiyomi.domain.category.interactor.GetCategories
|
||||||
|
import tachiyomi.domain.history.interactor.GetHistory
|
||||||
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class MangaBackupCreator(
|
||||||
|
private val handler: DatabaseHandler = Injekt.get(),
|
||||||
|
private val getCategories: GetCategories = Injekt.get(),
|
||||||
|
private val getHistory: GetHistory = Injekt.get(),
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun backupMangas(mangas: List<Manga>, flags: Int): List<BackupManga> {
|
||||||
|
return mangas.map {
|
||||||
|
backupManga(it, flags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun backupManga(manga: Manga, options: Int): BackupManga {
|
||||||
|
// Entry for this manga
|
||||||
|
val mangaObject = manga.toBackupManga()
|
||||||
|
|
||||||
|
// Check if user wants chapter information in backup
|
||||||
|
if (options and BackupCreateFlags.BACKUP_CHAPTER == BackupCreateFlags.BACKUP_CHAPTER) {
|
||||||
|
// Backup all the chapters
|
||||||
|
handler.awaitList {
|
||||||
|
chaptersQueries.getChaptersByMangaId(
|
||||||
|
mangaId = manga.id,
|
||||||
|
applyScanlatorFilter = 0, // false
|
||||||
|
mapper = backupChapterMapper,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.takeUnless(List<BackupChapter>::isEmpty)
|
||||||
|
?.let { mangaObject.chapters = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user wants category information in backup
|
||||||
|
if (options and BackupCreateFlags.BACKUP_CATEGORY == BackupCreateFlags.BACKUP_CATEGORY) {
|
||||||
|
// Backup categories for this manga
|
||||||
|
val categoriesForManga = getCategories.await(manga.id)
|
||||||
|
if (categoriesForManga.isNotEmpty()) {
|
||||||
|
mangaObject.categories = categoriesForManga.map { it.order }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user wants track information in backup
|
||||||
|
if (options and BackupCreateFlags.BACKUP_TRACK == BackupCreateFlags.BACKUP_TRACK) {
|
||||||
|
val tracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id, backupTrackMapper) }
|
||||||
|
if (tracks.isNotEmpty()) {
|
||||||
|
mangaObject.tracking = tracks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user wants history information in backup
|
||||||
|
if (options and BackupCreateFlags.BACKUP_HISTORY == BackupCreateFlags.BACKUP_HISTORY) {
|
||||||
|
val historyByMangaId = getHistory.await(manga.id)
|
||||||
|
if (historyByMangaId.isNotEmpty()) {
|
||||||
|
val history = historyByMangaId.map { history ->
|
||||||
|
val chapter = handler.awaitOne { chaptersQueries.getChapterById(history.chapterId) }
|
||||||
|
BackupHistory(chapter.url, history.readAt?.time ?: 0L, history.readDuration)
|
||||||
|
}
|
||||||
|
if (history.isNotEmpty()) {
|
||||||
|
mangaObject.history = history
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mangaObject
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Manga.toBackupManga() =
|
||||||
|
BackupManga(
|
||||||
|
url = this.url,
|
||||||
|
title = this.title,
|
||||||
|
artist = this.artist,
|
||||||
|
author = this.author,
|
||||||
|
description = this.description,
|
||||||
|
genre = this.genre.orEmpty(),
|
||||||
|
status = this.status.toInt(),
|
||||||
|
thumbnailUrl = this.thumbnailUrl,
|
||||||
|
favorite = this.favorite,
|
||||||
|
source = this.source,
|
||||||
|
dateAdded = this.dateAdded,
|
||||||
|
viewer = (this.viewerFlags.toInt() and ReadingMode.MASK),
|
||||||
|
viewer_flags = this.viewerFlags.toInt(),
|
||||||
|
chapterFlags = this.chapterFlags.toInt(),
|
||||||
|
updateStrategy = this.updateStrategy,
|
||||||
|
lastModifiedAt = this.lastModifiedAt,
|
||||||
|
favoriteModifiedAt = this.favoriteModifiedAt,
|
||||||
|
)
|
@ -0,0 +1,59 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.create.creators
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BooleanPreferenceValue
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.FloatPreferenceValue
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.IntPreferenceValue
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.LongPreferenceValue
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.StringPreferenceValue
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.StringSetPreferenceValue
|
||||||
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
|
import eu.kanade.tachiyomi.source.preferenceKey
|
||||||
|
import eu.kanade.tachiyomi.source.sourcePreferences
|
||||||
|
import tachiyomi.core.preference.Preference
|
||||||
|
import tachiyomi.core.preference.PreferenceStore
|
||||||
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class PreferenceBackupCreator(
|
||||||
|
private val sourceManager: SourceManager = Injekt.get(),
|
||||||
|
private val preferenceStore: PreferenceStore = Injekt.get(),
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun backupAppPreferences(): List<BackupPreference> {
|
||||||
|
return preferenceStore.getAll().toBackupPreferences()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun backupSourcePreferences(): List<BackupSourcePreferences> {
|
||||||
|
return sourceManager.getCatalogueSources()
|
||||||
|
.filterIsInstance<ConfigurableSource>()
|
||||||
|
.map {
|
||||||
|
BackupSourcePreferences(
|
||||||
|
it.preferenceKey(),
|
||||||
|
it.sourcePreferences().all.toBackupPreferences(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
private fun Map<String, *>.toBackupPreferences(): List<BackupPreference> {
|
||||||
|
return this.filterKeys {
|
||||||
|
!Preference.isPrivate(it) && !Preference.isAppState(it)
|
||||||
|
}
|
||||||
|
.mapNotNull { (key, value) ->
|
||||||
|
when (value) {
|
||||||
|
is Int -> BackupPreference(key, IntPreferenceValue(value))
|
||||||
|
is Long -> BackupPreference(key, LongPreferenceValue(value))
|
||||||
|
is Float -> BackupPreference(key, FloatPreferenceValue(value))
|
||||||
|
is String -> BackupPreference(key, StringPreferenceValue(value))
|
||||||
|
is Boolean -> BackupPreference(key, BooleanPreferenceValue(value))
|
||||||
|
is Set<*> -> (value as? Set<String>)?.let {
|
||||||
|
BackupPreference(key, StringSetPreferenceValue(it))
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.create.creators
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BackupSource
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class SourcesBackupCreator(
|
||||||
|
private val sourceManager: SourceManager = Injekt.get(),
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun backupSources(mangas: List<Manga>): List<BackupSource> {
|
||||||
|
return mangas
|
||||||
|
.asSequence()
|
||||||
|
.map(Manga::source)
|
||||||
|
.distinct()
|
||||||
|
.map(sourceManager::getOrStub)
|
||||||
|
.map { it.toBackupSource() }
|
||||||
|
.toList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Source.toBackupSource() =
|
||||||
|
BackupSource(
|
||||||
|
name = this.name,
|
||||||
|
sourceId = this.id,
|
||||||
|
)
|
@ -1,11 +1,11 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.models
|
package eu.kanade.tachiyomi.data.backup.models
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.Serializer
|
||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Date
|
@Serializer(forClass = Backup::class)
|
||||||
import java.util.Locale
|
object BackupSerializer
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Backup(
|
data class Backup(
|
||||||
@ -15,14 +15,4 @@ data class Backup(
|
|||||||
@ProtoNumber(101) var backupSources: List<BackupSource> = emptyList(),
|
@ProtoNumber(101) var backupSources: List<BackupSource> = emptyList(),
|
||||||
@ProtoNumber(104) var backupPreferences: List<BackupPreference> = emptyList(),
|
@ProtoNumber(104) var backupPreferences: List<BackupPreference> = emptyList(),
|
||||||
@ProtoNumber(105) var backupSourcePreferences: List<BackupSourcePreferences> = emptyList(),
|
@ProtoNumber(105) var backupSourcePreferences: List<BackupSourcePreferences> = emptyList(),
|
||||||
) {
|
)
|
||||||
|
|
||||||
companion object {
|
|
||||||
val filenameRegex = """${BuildConfig.APPLICATION_ID}_\d+-\d+-\d+_\d+-\d+.tachibk""".toRegex()
|
|
||||||
|
|
||||||
fun getFilename(): String {
|
|
||||||
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
|
|
||||||
return "${BuildConfig.APPLICATION_ID}_$date.tachibk"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.models
|
package eu.kanade.tachiyomi.data.backup.models
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
@ -60,28 +59,4 @@ data class BackupManga(
|
|||||||
favoriteModifiedAt = this@BackupManga.favoriteModifiedAt,
|
favoriteModifiedAt = this@BackupManga.favoriteModifiedAt,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun copyFrom(manga: Manga): BackupManga {
|
|
||||||
return BackupManga(
|
|
||||||
url = manga.url,
|
|
||||||
title = manga.title,
|
|
||||||
artist = manga.artist,
|
|
||||||
author = manga.author,
|
|
||||||
description = manga.description,
|
|
||||||
genre = manga.genre.orEmpty(),
|
|
||||||
status = manga.status.toInt(),
|
|
||||||
thumbnailUrl = manga.thumbnailUrl,
|
|
||||||
favorite = manga.favorite,
|
|
||||||
source = manga.source,
|
|
||||||
dateAdded = manga.dateAdded,
|
|
||||||
viewer = (manga.viewerFlags.toInt() and ReadingMode.MASK),
|
|
||||||
viewer_flags = manga.viewerFlags.toInt(),
|
|
||||||
chapterFlags = manga.chapterFlags.toInt(),
|
|
||||||
updateStrategy = manga.updateStrategy,
|
|
||||||
lastModifiedAt = manga.lastModifiedAt,
|
|
||||||
favoriteModifiedAt = manga.favoriteModifiedAt,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.models
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializer
|
|
||||||
|
|
||||||
@Serializer(forClass = Backup::class)
|
|
||||||
object BackupSerializer
|
|
@ -1,9 +1,14 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.models
|
package eu.kanade.tachiyomi.data.backup.models
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BackupSource(
|
||||||
|
@ProtoNumber(1) var name: String = "",
|
||||||
|
@ProtoNumber(2) var sourceId: Long,
|
||||||
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BrokenBackupSource(
|
data class BrokenBackupSource(
|
||||||
@ProtoNumber(0) var name: String = "",
|
@ProtoNumber(0) var name: String = "",
|
||||||
@ -11,18 +16,3 @@ data class BrokenBackupSource(
|
|||||||
) {
|
) {
|
||||||
fun toBackupSource() = BackupSource(name, sourceId)
|
fun toBackupSource() = BackupSource(name, sourceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class BackupSource(
|
|
||||||
@ProtoNumber(1) var name: String = "",
|
|
||||||
@ProtoNumber(2) var sourceId: Long,
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
fun copyFrom(source: Source): BackupSource {
|
|
||||||
return BackupSource(
|
|
||||||
name = source.name,
|
|
||||||
sourceId = source.id,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -7,7 +7,6 @@ import tachiyomi.domain.track.model.Track
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class BackupTracking(
|
data class BackupTracking(
|
||||||
// in 1.x some of these values have different types or names
|
// in 1.x some of these values have different types or names
|
||||||
// syncId is called siteId in 1,x
|
|
||||||
@ProtoNumber(1) var syncId: Int,
|
@ProtoNumber(1) var syncId: Int,
|
||||||
// LibraryId is not null in 1.x
|
// LibraryId is not null in 1.x
|
||||||
@ProtoNumber(2) var libraryId: Long,
|
@ProtoNumber(2) var libraryId: Long,
|
||||||
@ -34,7 +33,7 @@ data class BackupTracking(
|
|||||||
return Track(
|
return Track(
|
||||||
id = -1,
|
id = -1,
|
||||||
mangaId = -1,
|
mangaId = -1,
|
||||||
syncId = this@BackupTracking.syncId.toLong(),
|
trackerId = this@BackupTracking.syncId.toLong(),
|
||||||
remoteId = if (this@BackupTracking.mediaIdInt != 0) {
|
remoteId = if (this@BackupTracking.mediaIdInt != 0) {
|
||||||
this@BackupTracking.mediaIdInt.toLong()
|
this@BackupTracking.mediaIdInt.toLong()
|
||||||
} else {
|
} else {
|
||||||
|
@ -30,13 +30,19 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
|
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
|
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
|
||||||
?: return Result.failure()
|
val options = inputData.getBooleanArray(OPTIONS_KEY)
|
||||||
|
?.let { RestoreOptions.fromBooleanArray(it) }
|
||||||
|
|
||||||
|
if (uri == null || options == null) {
|
||||||
|
return Result.failure()
|
||||||
|
}
|
||||||
|
|
||||||
val isSync = inputData.getBoolean(SYNC_KEY, false)
|
val isSync = inputData.getBoolean(SYNC_KEY, false)
|
||||||
|
|
||||||
setForegroundSafely()
|
setForegroundSafely()
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
BackupRestorer(context, notifier, isSync).restore(uri)
|
BackupRestorer(context, notifier, isSync).restore(uri, options)
|
||||||
Result.success()
|
Result.success()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (e is CancellationException) {
|
if (e is CancellationException) {
|
||||||
@ -69,10 +75,16 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
return context.workManager.isRunning(TAG)
|
return context.workManager.isRunning(TAG)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun start(context: Context, uri: Uri, sync: Boolean = false) {
|
fun start(
|
||||||
|
context: Context,
|
||||||
|
uri: Uri,
|
||||||
|
options: RestoreOptions,
|
||||||
|
sync: Boolean = false,
|
||||||
|
) {
|
||||||
val inputData = workDataOf(
|
val inputData = workDataOf(
|
||||||
LOCATION_URI_KEY to uri.toString(),
|
LOCATION_URI_KEY to uri.toString(),
|
||||||
SYNC_KEY to sync,
|
SYNC_KEY to sync,
|
||||||
|
OPTIONS_KEY to options.toBooleanArray(),
|
||||||
)
|
)
|
||||||
val request = OneTimeWorkRequestBuilder<BackupRestoreJob>()
|
val request = OneTimeWorkRequestBuilder<BackupRestoreJob>()
|
||||||
.addTag(TAG)
|
.addTag(TAG)
|
||||||
@ -91,3 +103,4 @@ private const val TAG = "BackupRestore"
|
|||||||
|
|
||||||
private const val LOCATION_URI_KEY = "location_uri" // String
|
private const val LOCATION_URI_KEY = "location_uri" // String
|
||||||
private const val SYNC_KEY = "sync" // Boolean
|
private const val SYNC_KEY = "sync" // Boolean
|
||||||
|
private const val OPTIONS_KEY = "options" // BooleanArray
|
||||||
|
@ -2,12 +2,15 @@ package eu.kanade.tachiyomi.data.backup.restore
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupDecoder
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
|
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
|
import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
|
||||||
import eu.kanade.tachiyomi.util.BackupUtil
|
import eu.kanade.tachiyomi.data.backup.restore.restorers.CategoriesRestorer
|
||||||
|
import eu.kanade.tachiyomi.data.backup.restore.restorers.MangaRestorer
|
||||||
|
import eu.kanade.tachiyomi.data.backup.restore.restorers.PreferenceRestorer
|
||||||
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
@ -39,10 +42,10 @@ class BackupRestorer(
|
|||||||
*/
|
*/
|
||||||
private var sourceMapping: Map<Long, String> = emptyMap()
|
private var sourceMapping: Map<Long, String> = emptyMap()
|
||||||
|
|
||||||
suspend fun restore(uri: Uri) {
|
suspend fun restore(uri: Uri, options: RestoreOptions) {
|
||||||
val startTime = System.currentTimeMillis()
|
val startTime = System.currentTimeMillis()
|
||||||
|
|
||||||
restoreFromFile(uri)
|
restoreFromFile(uri, options)
|
||||||
|
|
||||||
val time = System.currentTimeMillis() - startTime
|
val time = System.currentTimeMillis() - startTime
|
||||||
|
|
||||||
@ -57,20 +60,36 @@ class BackupRestorer(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun restoreFromFile(uri: Uri) {
|
private suspend fun restoreFromFile(uri: Uri, options: RestoreOptions) {
|
||||||
val backup = BackupUtil.decodeBackup(context, uri)
|
val backup = BackupDecoder(context).decode(uri)
|
||||||
|
|
||||||
restoreAmount = backup.backupManga.size + 3 // +3 for categories, app prefs, source prefs
|
|
||||||
|
|
||||||
// Store source mapping for error messages
|
// Store source mapping for error messages
|
||||||
val backupMaps = backup.backupSources + backup.backupBrokenSources.map { it.toBackupSource() }
|
val backupMaps = backup.backupSources + backup.backupBrokenSources.map { it.toBackupSource() }
|
||||||
sourceMapping = backupMaps.associate { it.sourceId to it.name }
|
sourceMapping = backupMaps.associate { it.sourceId to it.name }
|
||||||
|
|
||||||
|
if (options.library) {
|
||||||
|
restoreAmount += backup.backupManga.size + 1 // +1 for categories
|
||||||
|
}
|
||||||
|
if (options.appSettings) {
|
||||||
|
restoreAmount += 1
|
||||||
|
}
|
||||||
|
if (options.sourceSettings) {
|
||||||
|
restoreAmount += 1
|
||||||
|
}
|
||||||
|
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
|
if (options.library) {
|
||||||
restoreCategories(backup.backupCategories)
|
restoreCategories(backup.backupCategories)
|
||||||
|
}
|
||||||
|
if (options.appSettings) {
|
||||||
restoreAppPreferences(backup.backupPreferences)
|
restoreAppPreferences(backup.backupPreferences)
|
||||||
|
}
|
||||||
|
if (options.sourceSettings) {
|
||||||
restoreSourcePreferences(backup.backupSourcePreferences)
|
restoreSourcePreferences(backup.backupSourcePreferences)
|
||||||
|
}
|
||||||
|
if (options.library) {
|
||||||
restoreManga(backup.backupManga, backup.backupCategories)
|
restoreManga(backup.backupManga, backup.backupCategories)
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: optionally trigger online library + tracker update
|
// TODO: optionally trigger online library + tracker update
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.restore
|
||||||
|
|
||||||
|
data class RestoreOptions(
|
||||||
|
val appSettings: Boolean = true,
|
||||||
|
val sourceSettings: Boolean = true,
|
||||||
|
val library: Boolean = true,
|
||||||
|
) {
|
||||||
|
fun toBooleanArray() = booleanArrayOf(appSettings, sourceSettings, library)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromBooleanArray(booleanArray: BooleanArray) = RestoreOptions(
|
||||||
|
appSettings = booleanArray[0],
|
||||||
|
sourceSettings = booleanArray[1],
|
||||||
|
library = booleanArray[2],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.restore
|
package eu.kanade.tachiyomi.data.backup.restore.restorers
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
||||||
import tachiyomi.data.DatabaseHandler
|
import tachiyomi.data.DatabaseHandler
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.restore
|
package eu.kanade.tachiyomi.data.backup.restore.restorers
|
||||||
|
|
||||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
||||||
@ -329,7 +329,7 @@ class MangaRestorer(
|
|||||||
readAt = max(item.readAt?.time ?: 0L, dbHistory.last_read?.time ?: 0L)
|
readAt = max(item.readAt?.time ?: 0L, dbHistory.last_read?.time ?: 0L)
|
||||||
.takeIf { it > 0L }
|
.takeIf { it > 0L }
|
||||||
?.let { Date(it) },
|
?.let { Date(it) },
|
||||||
readDuration = max(item.readDuration, dbHistory.time_read),
|
readDuration = max(item.readDuration, dbHistory.time_read) - dbHistory.time_read,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -347,12 +347,12 @@ class MangaRestorer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun restoreTracking(manga: Manga, backupTracks: List<BackupTracking>) {
|
private suspend fun restoreTracking(manga: Manga, backupTracks: List<BackupTracking>) {
|
||||||
val dbTrackBySyncId = getTracks.await(manga.id).associateBy { it.syncId }
|
val dbTrackByTrackerId = getTracks.await(manga.id).associateBy { it.trackerId }
|
||||||
|
|
||||||
val (existingTracks, newTracks) = backupTracks
|
val (existingTracks, newTracks) = backupTracks
|
||||||
.mapNotNull {
|
.mapNotNull {
|
||||||
val track = it.getTrackImpl()
|
val track = it.getTrackImpl()
|
||||||
val dbTrack = dbTrackBySyncId[track.syncId]
|
val dbTrack = dbTrackByTrackerId[track.trackerId]
|
||||||
?: // New track
|
?: // New track
|
||||||
return@mapNotNull track.copy(
|
return@mapNotNull track.copy(
|
||||||
id = 0, // Let DB assign new ID
|
id = 0, // Let DB assign new ID
|
||||||
@ -381,7 +381,7 @@ class MangaRestorer(
|
|||||||
existingTracks.forEach { track ->
|
existingTracks.forEach { track ->
|
||||||
manga_syncQueries.update(
|
manga_syncQueries.update(
|
||||||
track.mangaId,
|
track.mangaId,
|
||||||
track.syncId,
|
track.trackerId,
|
||||||
track.remoteId,
|
track.remoteId,
|
||||||
track.libraryId,
|
track.libraryId,
|
||||||
track.title,
|
track.title,
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.restore
|
package eu.kanade.tachiyomi.data.backup.restore.restorers
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
|
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
|
@ -14,7 +14,6 @@ import okio.buffer
|
|||||||
import okio.sink
|
import okio.sink
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
@ -26,9 +25,10 @@ import java.io.IOException
|
|||||||
*
|
*
|
||||||
* @param context the application context.
|
* @param context the application context.
|
||||||
*/
|
*/
|
||||||
class ChapterCache(private val context: Context) {
|
class ChapterCache(
|
||||||
|
private val context: Context,
|
||||||
private val json: Json by injectLazy()
|
private val json: Json,
|
||||||
|
) {
|
||||||
|
|
||||||
/** Cache class used for cache management. */
|
/** Cache class used for cache management. */
|
||||||
private val diskCache = DiskLruCache.open(
|
private val diskCache = DiskLruCache.open(
|
||||||
|
@ -8,9 +8,9 @@ interface Track : Serializable {
|
|||||||
|
|
||||||
var manga_id: Long
|
var manga_id: Long
|
||||||
|
|
||||||
var sync_id: Int
|
var tracker_id: Int
|
||||||
|
|
||||||
var media_id: Long
|
var remote_id: Long
|
||||||
|
|
||||||
var library_id: Long?
|
var library_id: Long?
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ interface Track : Serializable {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun create(serviceId: Long): Track = TrackImpl().apply {
|
fun create(serviceId: Long): Track = TrackImpl().apply {
|
||||||
sync_id = serviceId.toInt()
|
tracker_id = serviceId.toInt()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,9 +6,9 @@ class TrackImpl : Track {
|
|||||||
|
|
||||||
override var manga_id: Long = 0
|
override var manga_id: Long = 0
|
||||||
|
|
||||||
override var sync_id: Int = 0
|
override var tracker_id: Int = 0
|
||||||
|
|
||||||
override var media_id: Long = 0
|
override var remote_id: Long = 0
|
||||||
|
|
||||||
override var library_id: Long? = null
|
override var library_id: Long? = null
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ class DownloadProvider(
|
|||||||
private val storageManager: StorageManager = Injekt.get(),
|
private val storageManager: StorageManager = Injekt.get(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val downloadsDir: UniFile?
|
private val downloadsDir: UniFile?
|
||||||
get() = storageManager.getDownloadsDirectory()
|
get() = storageManager.getDownloadsDirectory()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -59,6 +59,7 @@ import uy.kohesive.injekt.Injekt
|
|||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.BufferedOutputStream
|
import java.io.BufferedOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.Locale
|
||||||
import java.util.zip.CRC32
|
import java.util.zip.CRC32
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipOutputStream
|
import java.util.zip.ZipOutputStream
|
||||||
@ -422,7 +423,7 @@ class Downloader(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val digitCount = (download.pages?.size ?: 0).toString().length.coerceAtLeast(3)
|
val digitCount = (download.pages?.size ?: 0).toString().length.coerceAtLeast(3)
|
||||||
val filename = String.format("%0${digitCount}d", page.number)
|
val filename = "%0${digitCount}d".format(Locale.ENGLISH, page.number)
|
||||||
val tmpFile = tmpDir.findFile("$filename.tmp")
|
val tmpFile = tmpDir.findFile("$filename.tmp")
|
||||||
|
|
||||||
// Delete temp file if it exists
|
// Delete temp file if it exists
|
||||||
@ -537,7 +538,7 @@ class Downloader(
|
|||||||
if (!downloadPreferences.splitTallImages().get()) return
|
if (!downloadPreferences.splitTallImages().get()) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val filenamePrefix = String.format("%03d", page.number)
|
val filenamePrefix = "%03d".format(Locale.ENGLISH, page.number)
|
||||||
val imageFile = tmpDir.listFiles()?.firstOrNull { it.name.orEmpty().startsWith(filenamePrefix) }
|
val imageFile = tmpDir.listFiles()?.firstOrNull { it.name.orEmpty().startsWith(filenamePrefix) }
|
||||||
?: error(context.stringResource(MR.strings.download_notifier_split_page_not_found, page.number))
|
?: error(context.stringResource(MR.strings.download_notifier_split_page_not_found, page.number))
|
||||||
|
|
||||||
@ -579,11 +580,7 @@ class Downloader(
|
|||||||
else -> true
|
else -> true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (downloadedImagesCount != downloadPageCount) {
|
return downloadedImagesCount == downloadPageCount
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -235,7 +235,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
.map { (reason, entries) -> "$reason: [${entries.map { it.first.title }.sorted().joinToString()}]" }
|
.map { (reason, entries) -> "$reason: [${entries.map { it.first.title }.sorted().joinToString()}]" }
|
||||||
.joinToString()
|
.joinToString()
|
||||||
}
|
}
|
||||||
notifier.showUpdateSkippedNotification(skippedUpdates.size)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,27 +139,6 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows notification containing update entries that were skipped.
|
|
||||||
*
|
|
||||||
* @param skipped Number of entries that were skipped during the update.
|
|
||||||
*/
|
|
||||||
fun showUpdateSkippedNotification(skipped: Int) {
|
|
||||||
if (skipped == 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
context.notify(
|
|
||||||
Notifications.ID_LIBRARY_SKIPPED,
|
|
||||||
Notifications.CHANNEL_LIBRARY_SKIPPED,
|
|
||||||
) {
|
|
||||||
setContentTitle(context.stringResource(MR.strings.notification_update_skipped, skipped))
|
|
||||||
setContentText(context.stringResource(MR.strings.learn_more))
|
|
||||||
setSmallIcon(R.drawable.ic_tachi)
|
|
||||||
setContentIntent(NotificationHandler.openUrl(context, HELP_SKIPPED_URL))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows the notification containing the result of the update done by the service.
|
* Shows the notification containing the result of the update done by the service.
|
||||||
*
|
*
|
||||||
@ -246,7 +225,7 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
|
|
||||||
// Mark chapters as read action
|
// Mark chapters as read action
|
||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_glasses_24dp,
|
R.drawable.ic_done_24dp,
|
||||||
context.stringResource(MR.strings.action_mark_as_read),
|
context.stringResource(MR.strings.action_mark_as_read),
|
||||||
NotificationReceiver.markAsReadPendingBroadcast(
|
NotificationReceiver.markAsReadPendingBroadcast(
|
||||||
context,
|
context,
|
||||||
@ -385,4 +364,3 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
private const val NOTIF_MAX_CHAPTERS = 5
|
private const val NOTIF_MAX_CHAPTERS = 5
|
||||||
private const val NOTIF_TITLE_MAX_LEN = 45
|
private const val NOTIF_TITLE_MAX_LEN = 45
|
||||||
private const val NOTIF_ICON_SIZE = 192
|
private const val NOTIF_ICON_SIZE = 192
|
||||||
private const val HELP_SKIPPED_URL = "https://tachiyomi.org/docs/faq/library#why-is-global-update-skipping-entries"
|
|
||||||
|
@ -247,7 +247,6 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
private const val NAME = "NotificationReceiver"
|
private const val NAME = "NotificationReceiver"
|
||||||
|
|
||||||
private const val ACTION_SHARE_IMAGE = "$ID.$NAME.SHARE_IMAGE"
|
private const val ACTION_SHARE_IMAGE = "$ID.$NAME.SHARE_IMAGE"
|
||||||
private const val ACTION_DELETE_IMAGE = "$ID.$NAME.DELETE_IMAGE"
|
|
||||||
|
|
||||||
private const val ACTION_SHARE_BACKUP = "$ID.$NAME.SEND_BACKUP"
|
private const val ACTION_SHARE_BACKUP = "$ID.$NAME.SEND_BACKUP"
|
||||||
|
|
||||||
@ -270,7 +269,6 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
|
|
||||||
private const val ACTION_DISMISS_NOTIFICATION = "$ID.$NAME.ACTION_DISMISS_NOTIFICATION"
|
private const val ACTION_DISMISS_NOTIFICATION = "$ID.$NAME.ACTION_DISMISS_NOTIFICATION"
|
||||||
|
|
||||||
private const val EXTRA_FILE_LOCATION = "$ID.$NAME.FILE_LOCATION"
|
|
||||||
private const val EXTRA_URI = "$ID.$NAME.URI"
|
private const val EXTRA_URI = "$ID.$NAME.URI"
|
||||||
private const val EXTRA_NOTIFICATION_ID = "$ID.$NAME.NOTIFICATION_ID"
|
private const val EXTRA_NOTIFICATION_ID = "$ID.$NAME.NOTIFICATION_ID"
|
||||||
private const val EXTRA_GROUP_ID = "$ID.$NAME.EXTRA_GROUP_ID"
|
private const val EXTRA_GROUP_ID = "$ID.$NAME.EXTRA_GROUP_ID"
|
||||||
@ -375,7 +373,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
it.id == notificationId
|
it.id == notificationId
|
||||||
}?.groupKey
|
}?.groupKey
|
||||||
|
|
||||||
if (groupId != null && groupId != 0 && groupKey != null && groupKey.isNotEmpty()) {
|
if (groupId != null && groupId != 0 && !groupKey.isNullOrEmpty()) {
|
||||||
val notifications = context.notificationManager.activeNotifications.filter {
|
val notifications = context.notificationManager.activeNotifications.filter {
|
||||||
it.groupKey == groupKey
|
it.groupKey == groupKey
|
||||||
}
|
}
|
||||||
|
@ -30,8 +30,6 @@ object Notifications {
|
|||||||
const val ID_LIBRARY_SIZE_WARNING = -103
|
const val ID_LIBRARY_SIZE_WARNING = -103
|
||||||
const val CHANNEL_LIBRARY_ERROR = "library_errors_channel"
|
const val CHANNEL_LIBRARY_ERROR = "library_errors_channel"
|
||||||
const val ID_LIBRARY_ERROR = -102
|
const val ID_LIBRARY_ERROR = -102
|
||||||
const val CHANNEL_LIBRARY_SKIPPED = "library_skipped_channel"
|
|
||||||
const val ID_LIBRARY_SKIPPED = -104
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notification channel and ids used by the downloader.
|
* Notification channel and ids used by the downloader.
|
||||||
@ -86,6 +84,7 @@ object Notifications {
|
|||||||
"updates_ext_channel",
|
"updates_ext_channel",
|
||||||
"downloader_cache_renewal",
|
"downloader_cache_renewal",
|
||||||
"crash_logs_channel",
|
"crash_logs_channel",
|
||||||
|
"library_skipped_channel",
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -132,11 +131,6 @@ object Notifications {
|
|||||||
setGroup(GROUP_LIBRARY)
|
setGroup(GROUP_LIBRARY)
|
||||||
setShowBadge(false)
|
setShowBadge(false)
|
||||||
},
|
},
|
||||||
buildNotificationChannel(CHANNEL_LIBRARY_SKIPPED, IMPORTANCE_LOW) {
|
|
||||||
setName(context.stringResource(MR.strings.channel_skipped))
|
|
||||||
setGroup(GROUP_LIBRARY)
|
|
||||||
setShowBadge(false)
|
|
||||||
},
|
|
||||||
buildNotificationChannel(CHANNEL_NEW_CHAPTERS, IMPORTANCE_DEFAULT) {
|
buildNotificationChannel(CHANNEL_NEW_CHAPTERS, IMPORTANCE_DEFAULT) {
|
||||||
setName(context.stringResource(MR.strings.channel_new_chapters))
|
setName(context.stringResource(MR.strings.channel_new_chapters))
|
||||||
},
|
},
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
package eu.kanade.tachiyomi.data.track
|
package eu.kanade.tachiyomi.data.track
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import tachiyomi.domain.track.model.Track
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tracker that support deleting am entry from a user's list.
|
* Tracker that support deleting am entry from a user's list.
|
||||||
*/
|
*/
|
||||||
interface DeletableTracker {
|
interface DeletableTracker {
|
||||||
|
|
||||||
suspend fun delete(track: Track): Track
|
suspend fun delete(track: Track)
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.data.database.models.Track
|
|||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||||
|
|
||||||
interface Tracker {
|
interface Tracker {
|
||||||
|
|
||||||
@ -39,11 +40,11 @@ interface Tracker {
|
|||||||
fun getScoreList(): ImmutableList<String>
|
fun getScoreList(): ImmutableList<String>
|
||||||
|
|
||||||
// TODO: Store all scores as 10 point in the future maybe?
|
// TODO: Store all scores as 10 point in the future maybe?
|
||||||
fun get10PointScore(track: tachiyomi.domain.track.model.Track): Double
|
fun get10PointScore(track: DomainTrack): Double
|
||||||
|
|
||||||
fun indexToScore(index: Int): Float
|
fun indexToScore(index: Int): Float
|
||||||
|
|
||||||
fun displayScore(track: Track): String
|
fun displayScore(track: DomainTrack): String
|
||||||
|
|
||||||
suspend fun update(track: Track, didReadChapter: Boolean = false): Track
|
suspend fun update(track: Track, didReadChapter: Boolean = false): Track
|
||||||
|
|
||||||
|
@ -33,6 +33,4 @@ class TrackerManager {
|
|||||||
fun loggedInTrackers() = trackers.filter { it.isLoggedIn }
|
fun loggedInTrackers() = trackers.filter { it.isLoggedIn }
|
||||||
|
|
||||||
fun get(id: Long) = trackers.find { it.id == id }
|
fun get(id: Long) = trackers.find { it.id == id }
|
||||||
|
|
||||||
fun hasLoggedIn() = trackers.any { it.isLoggedIn }
|
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.track.anilist
|
|||||||
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
|
import eu.kanade.domain.track.model.toDbTrack
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.BaseTracker
|
import eu.kanade.tachiyomi.data.track.BaseTracker
|
||||||
@ -120,16 +121,16 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun displayScore(track: Track): String {
|
override fun displayScore(track: DomainTrack): String {
|
||||||
val score = track.score
|
val score = track.score
|
||||||
|
|
||||||
return when (scorePreference.get()) {
|
return when (scorePreference.get()) {
|
||||||
POINT_5 -> when (score) {
|
POINT_5 -> when (score) {
|
||||||
0f -> "0 ★"
|
0.0 -> "0 ★"
|
||||||
else -> "${((score + 10) / 20).toInt()} ★"
|
else -> "${((score + 10) / 20).toInt()} ★"
|
||||||
}
|
}
|
||||||
POINT_3 -> when {
|
POINT_3 -> when {
|
||||||
score == 0f -> "0"
|
score == 0.0 -> "0"
|
||||||
score <= 35 -> "😦"
|
score <= 35 -> "😦"
|
||||||
score <= 60 -> "😐"
|
score <= 60 -> "😐"
|
||||||
else -> "😊"
|
else -> "😊"
|
||||||
@ -167,13 +168,13 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
|
|||||||
return api.updateLibManga(track)
|
return api.updateLibManga(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun delete(track: Track): Track {
|
override suspend fun delete(track: DomainTrack) {
|
||||||
if (track.library_id == null || track.library_id!! == 0L) {
|
if (track.libraryId == null || track.libraryId == 0L) {
|
||||||
val libManga = api.findLibManga(track, getUsername().toInt()) ?: return track
|
val libManga = api.findLibManga(track.toDbTrack(), getUsername().toInt()) ?: return
|
||||||
track.library_id = libManga.library_id
|
return api.deleteLibManga(track.copy(id = libManga.library_id!!))
|
||||||
}
|
}
|
||||||
|
|
||||||
return api.deleteLibManga(track)
|
api.deleteLibManga(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
|
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
|
||||||
|
@ -31,6 +31,7 @@ import java.time.LocalDate
|
|||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
import kotlin.time.Duration.Companion.minutes
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||||
|
|
||||||
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||||
|
|
||||||
@ -55,7 +56,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
val payload = buildJsonObject {
|
val payload = buildJsonObject {
|
||||||
put("query", query)
|
put("query", query)
|
||||||
putJsonObject("variables") {
|
putJsonObject("variables") {
|
||||||
put("mangaId", track.media_id)
|
put("mangaId", track.remote_id)
|
||||||
put("progress", track.last_chapter_read.toInt())
|
put("progress", track.last_chapter_read.toInt())
|
||||||
put("status", track.toAnilistStatus())
|
put("status", track.toAnilistStatus())
|
||||||
}
|
}
|
||||||
@ -113,8 +114,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun deleteLibManga(track: Track): Track {
|
suspend fun deleteLibManga(track: DomainTrack) {
|
||||||
return withIOContext {
|
withIOContext {
|
||||||
val query = """
|
val query = """
|
||||||
|mutation DeleteManga(${'$'}listId: Int) {
|
|mutation DeleteManga(${'$'}listId: Int) {
|
||||||
|DeleteMediaListEntry(id: ${'$'}listId) {
|
|DeleteMediaListEntry(id: ${'$'}listId) {
|
||||||
@ -126,12 +127,11 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
val payload = buildJsonObject {
|
val payload = buildJsonObject {
|
||||||
put("query", query)
|
put("query", query)
|
||||||
putJsonObject("variables") {
|
putJsonObject("variables") {
|
||||||
put("listId", track.library_id)
|
put("listId", track.libraryId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime)))
|
authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime)))
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
track
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
suspend fun search(search: String): List<TrackSearch> {
|
suspend fun search(search: String): List<TrackSearch> {
|
||||||
@ -235,7 +235,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
put("query", query)
|
put("query", query)
|
||||||
putJsonObject("variables") {
|
putJsonObject("variables") {
|
||||||
put("id", userid)
|
put("id", userid)
|
||||||
put("manga_id", track.media_id)
|
put("manga_id", track.remote_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
with(json) {
|
with(json) {
|
||||||
@ -258,8 +258,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getLibManga(track: Track, userid: Int): Track {
|
suspend fun getLibManga(track: Track, userId: Int): Track {
|
||||||
return findLibManga(track, userid) ?: throw Exception("Could not find manga")
|
return findLibManga(track, userId) ?: throw Exception("Could not find manga")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createOAuth(token: String): OAuth {
|
fun createOAuth(token: String): OAuth {
|
||||||
|
@ -9,9 +9,10 @@ import kotlinx.serialization.Serializable
|
|||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||||
|
|
||||||
data class ALManga(
|
data class ALManga(
|
||||||
val media_id: Long,
|
val remote_id: Long,
|
||||||
val title_user_pref: String,
|
val title_user_pref: String,
|
||||||
val image_url_lge: String,
|
val image_url_lge: String,
|
||||||
val description: String?,
|
val description: String?,
|
||||||
@ -23,13 +24,13 @@ data class ALManga(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
fun toTrack() = TrackSearch.create(TrackerManager.ANILIST).apply {
|
fun toTrack() = TrackSearch.create(TrackerManager.ANILIST).apply {
|
||||||
media_id = this@ALManga.media_id
|
remote_id = this@ALManga.remote_id
|
||||||
title = title_user_pref
|
title = title_user_pref
|
||||||
total_chapters = this@ALManga.total_chapters
|
total_chapters = this@ALManga.total_chapters
|
||||||
cover_url = image_url_lge
|
cover_url = image_url_lge
|
||||||
summary = description?.htmlDecode() ?: ""
|
summary = description?.htmlDecode() ?: ""
|
||||||
score = average_score.toFloat()
|
score = average_score.toFloat()
|
||||||
tracking_url = AnilistApi.mangaUrl(media_id)
|
tracking_url = AnilistApi.mangaUrl(remote_id)
|
||||||
publishing_status = this@ALManga.publishing_status
|
publishing_status = this@ALManga.publishing_status
|
||||||
publishing_type = format
|
publishing_type = format
|
||||||
if (start_date_fuzzy != 0L) {
|
if (start_date_fuzzy != 0L) {
|
||||||
@ -54,7 +55,7 @@ data class ALUserManga(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
fun toTrack() = Track.create(TrackerManager.ANILIST).apply {
|
fun toTrack() = Track.create(TrackerManager.ANILIST).apply {
|
||||||
media_id = manga.media_id
|
remote_id = manga.remote_id
|
||||||
title = manga.title_user_pref
|
title = manga.title_user_pref
|
||||||
status = toTrackStatus()
|
status = toTrackStatus()
|
||||||
score = score_raw.toFloat()
|
score = score_raw.toFloat()
|
||||||
@ -98,28 +99,28 @@ fun Track.toAnilistStatus() = when (status) {
|
|||||||
|
|
||||||
private val preferences: TrackPreferences by injectLazy()
|
private val preferences: TrackPreferences by injectLazy()
|
||||||
|
|
||||||
fun Track.toAnilistScore(): String = when (preferences.anilistScoreType().get()) {
|
fun DomainTrack.toAnilistScore(): String = when (preferences.anilistScoreType().get()) {
|
||||||
// 10 point
|
// 10 point
|
||||||
"POINT_10" -> (score.toInt() / 10).toString()
|
"POINT_10" -> (score.toInt() / 10).toString()
|
||||||
// 100 point
|
// 100 point
|
||||||
"POINT_100" -> score.toInt().toString()
|
"POINT_100" -> score.toInt().toString()
|
||||||
// 5 stars
|
// 5 stars
|
||||||
"POINT_5" -> when {
|
"POINT_5" -> when {
|
||||||
score == 0f -> "0"
|
score == 0.0 -> "0"
|
||||||
score < 30 -> "1"
|
score < 30 -> "1"
|
||||||
score < 50 -> "2"
|
score < 50 -> "2"
|
||||||
score < 70 -> "3"
|
score < 70 -> "3"
|
||||||
score < 90 -> "4"
|
score < 90 -> "4"
|
||||||
else -> "5"
|
else -> "5"
|
||||||
}
|
}
|
||||||
// Smiley
|
// Smiley
|
||||||
"POINT_3" -> when {
|
"POINT_3" -> when {
|
||||||
score == 0f -> "0"
|
score == 0.0 -> "0"
|
||||||
score <= 35 -> ":("
|
score <= 35 -> ":("
|
||||||
score <= 60 -> ":|"
|
score <= 60 -> ":|"
|
||||||
else -> ":)"
|
else -> ":)"
|
||||||
}
|
}
|
||||||
// 10 point decimal
|
// 10 point decimal
|
||||||
"POINT_10_DECIMAL" -> (score / 10).toString()
|
"POINT_10_DECIMAL" -> (score / 10).toString()
|
||||||
else -> throw NotImplementedError("Unknown score type")
|
else -> throw NotImplementedError("Unknown score type")
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import kotlinx.serialization.encodeToString
|
|||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||||
|
|
||||||
class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
|
class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
|
||||||
|
|
||||||
@ -23,7 +24,7 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
|
|||||||
|
|
||||||
override fun getScoreList(): ImmutableList<String> = SCORE_LIST
|
override fun getScoreList(): ImmutableList<String> = SCORE_LIST
|
||||||
|
|
||||||
override fun displayScore(track: Track): String {
|
override fun displayScore(track: DomainTrack): String {
|
||||||
return track.score.toInt().toString()
|
return track.score.toInt().toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ class BangumiApi(
|
|||||||
.add("rating", track.score.toInt().toString())
|
.add("rating", track.score.toInt().toString())
|
||||||
.add("status", track.toBangumiStatus())
|
.add("status", track.toBangumiStatus())
|
||||||
.build()
|
.build()
|
||||||
authClient.newCall(POST("$apiUrl/collection/${track.media_id}/update", body = body))
|
authClient.newCall(POST("$apiUrl/collection/${track.remote_id}/update", body = body))
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
track
|
track
|
||||||
}
|
}
|
||||||
@ -55,7 +55,7 @@ class BangumiApi(
|
|||||||
.add("rating", track.score.toInt().toString())
|
.add("rating", track.score.toInt().toString())
|
||||||
.add("status", track.toBangumiStatus())
|
.add("status", track.toBangumiStatus())
|
||||||
.build()
|
.build()
|
||||||
authClient.newCall(POST("$apiUrl/collection/${track.media_id}/update", body = sbody))
|
authClient.newCall(POST("$apiUrl/collection/${track.remote_id}/update", body = sbody))
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
|
|
||||||
// chapter update
|
// chapter update
|
||||||
@ -64,7 +64,7 @@ class BangumiApi(
|
|||||||
.build()
|
.build()
|
||||||
authClient.newCall(
|
authClient.newCall(
|
||||||
POST(
|
POST(
|
||||||
"$apiUrl/subject/${track.media_id}/update/watched_eps",
|
"$apiUrl/subject/${track.remote_id}/update/watched_eps",
|
||||||
body = body,
|
body = body,
|
||||||
),
|
),
|
||||||
).awaitSuccess()
|
).awaitSuccess()
|
||||||
@ -111,7 +111,7 @@ class BangumiApi(
|
|||||||
}
|
}
|
||||||
val rating = obj["rating"]?.jsonObject?.get("score")?.jsonPrimitive?.floatOrNull ?: -1f
|
val rating = obj["rating"]?.jsonObject?.get("score")?.jsonPrimitive?.floatOrNull ?: -1f
|
||||||
return TrackSearch.create(trackId).apply {
|
return TrackSearch.create(trackId).apply {
|
||||||
media_id = obj["id"]!!.jsonPrimitive.long
|
remote_id = obj["id"]!!.jsonPrimitive.long
|
||||||
title = obj["name_cn"]!!.jsonPrimitive.content
|
title = obj["name_cn"]!!.jsonPrimitive.content
|
||||||
cover_url = coverUrl
|
cover_url = coverUrl
|
||||||
summary = obj["name"]!!.jsonPrimitive.content
|
summary = obj["name"]!!.jsonPrimitive.content
|
||||||
@ -124,7 +124,7 @@ class BangumiApi(
|
|||||||
suspend fun findLibManga(track: Track): Track? {
|
suspend fun findLibManga(track: Track): Track? {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
with(json) {
|
with(json) {
|
||||||
authClient.newCall(GET("$apiUrl/subject/${track.media_id}"))
|
authClient.newCall(GET("$apiUrl/subject/${track.remote_id}"))
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<JsonObject>()
|
.parseAs<JsonObject>()
|
||||||
.let { jsonToSearch(it) }
|
.let { jsonToSearch(it) }
|
||||||
@ -134,7 +134,7 @@ class BangumiApi(
|
|||||||
|
|
||||||
suspend fun statusLibManga(track: Track): Track? {
|
suspend fun statusLibManga(track: Track): Track? {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val urlUserRead = "$apiUrl/collection/${track.media_id}"
|
val urlUserRead = "$apiUrl/collection/${track.remote_id}"
|
||||||
val requestUserRead = Request.Builder()
|
val requestUserRead = Request.Builder()
|
||||||
.url(urlUserRead)
|
.url(urlUserRead)
|
||||||
.cacheControl(CacheControl.FORCE_NETWORK)
|
.cacheControl(CacheControl.FORCE_NETWORK)
|
||||||
|
@ -6,7 +6,7 @@ import okhttp3.Interceptor
|
|||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class BangumiInterceptor(val bangumi: Bangumi) : Interceptor {
|
class BangumiInterceptor(private val bangumi: Bangumi) : Interceptor {
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
@ -62,12 +62,3 @@ fun Track.toBangumiStatus() = when (status) {
|
|||||||
Bangumi.PLAN_TO_READ -> "wish"
|
Bangumi.PLAN_TO_READ -> "wish"
|
||||||
else -> throw NotImplementedError("Unknown status: $status")
|
else -> throw NotImplementedError("Unknown status: $status")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toTrackStatus(status: String) = when (status) {
|
|
||||||
"do" -> Bangumi.READING
|
|
||||||
"collect" -> Bangumi.COMPLETED
|
|
||||||
"on_hold" -> Bangumi.ON_HOLD
|
|
||||||
"dropped" -> Bangumi.DROPPED
|
|
||||||
"wish" -> Bangumi.PLAN_TO_READ
|
|
||||||
else -> throw NotImplementedError("Unknown status: $status")
|
|
||||||
}
|
|
||||||
|
@ -55,7 +55,7 @@ class Kavita(id: Long) : BaseTracker(id, "Kavita"), EnhancedTracker {
|
|||||||
|
|
||||||
override fun getScoreList(): ImmutableList<String> = persistentListOf()
|
override fun getScoreList(): ImmutableList<String> = persistentListOf()
|
||||||
|
|
||||||
override fun displayScore(track: Track): String = ""
|
override fun displayScore(track: DomainTrack): String = ""
|
||||||
|
|
||||||
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
|
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
|
||||||
if (track.status != COMPLETED) {
|
if (track.status != COMPLETED) {
|
||||||
|
@ -14,6 +14,7 @@ import kotlinx.serialization.json.Json
|
|||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
|
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||||
|
|
||||||
class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
|
class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
|
||||||
|
|
||||||
@ -65,7 +66,7 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
|
|||||||
return if (index > 0) (index + 1) / 2f else 0f
|
return if (index > 0) (index + 1) / 2f else 0f
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun displayScore(track: Track): String {
|
override fun displayScore(track: DomainTrack): String {
|
||||||
val df = DecimalFormat("0.#")
|
val df = DecimalFormat("0.#")
|
||||||
return df.format(track.score)
|
return df.format(track.score)
|
||||||
}
|
}
|
||||||
@ -92,15 +93,15 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
|
|||||||
return api.updateLibManga(track)
|
return api.updateLibManga(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun delete(track: Track): Track {
|
override suspend fun delete(track: DomainTrack) {
|
||||||
return api.removeLibManga(track)
|
api.removeLibManga(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
|
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
|
||||||
val remoteTrack = api.findLibManga(track, getUserId())
|
val remoteTrack = api.findLibManga(track, getUserId())
|
||||||
return if (remoteTrack != null) {
|
return if (remoteTrack != null) {
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.media_id = remoteTrack.media_id
|
track.remote_id = remoteTrack.remote_id
|
||||||
|
|
||||||
if (track.status != COMPLETED) {
|
if (track.status != COMPLETED) {
|
||||||
track.status = if (hasReadChapters) READING else track.status
|
track.status = if (hasReadChapters) READING else track.status
|
||||||
|
@ -29,6 +29,7 @@ import tachiyomi.core.util.lang.withIOContext
|
|||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
|
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||||
|
|
||||||
class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) {
|
class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) {
|
||||||
|
|
||||||
@ -54,7 +55,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
}
|
}
|
||||||
putJsonObject("media") {
|
putJsonObject("media") {
|
||||||
putJsonObject("data") {
|
putJsonObject("data") {
|
||||||
put("id", track.media_id)
|
put("id", track.remote_id)
|
||||||
put("type", "manga")
|
put("type", "manga")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -77,7 +78,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<JsonObject>()
|
.parseAs<JsonObject>()
|
||||||
.let {
|
.let {
|
||||||
track.media_id = it["data"]!!.jsonObject["id"]!!.jsonPrimitive.long
|
track.remote_id = it["data"]!!.jsonObject["id"]!!.jsonPrimitive.long
|
||||||
track
|
track
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -89,7 +90,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
val data = buildJsonObject {
|
val data = buildJsonObject {
|
||||||
putJsonObject("data") {
|
putJsonObject("data") {
|
||||||
put("type", "libraryEntries")
|
put("type", "libraryEntries")
|
||||||
put("id", track.media_id)
|
put("id", track.remote_id)
|
||||||
putJsonObject("attributes") {
|
putJsonObject("attributes") {
|
||||||
put("status", track.toKitsuStatus())
|
put("status", track.toKitsuStatus())
|
||||||
put("progress", track.last_chapter_read.toInt())
|
put("progress", track.last_chapter_read.toInt())
|
||||||
@ -103,7 +104,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
with(json) {
|
with(json) {
|
||||||
authClient.newCall(
|
authClient.newCall(
|
||||||
Request.Builder()
|
Request.Builder()
|
||||||
.url("${baseUrl}library-entries/${track.media_id}")
|
.url("${baseUrl}library-entries/${track.remote_id}")
|
||||||
.headers(
|
.headers(
|
||||||
headersOf(
|
headersOf(
|
||||||
"Content-Type",
|
"Content-Type",
|
||||||
@ -124,11 +125,12 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun removeLibManga(track: Track): Track {
|
suspend fun removeLibManga(track: DomainTrack) {
|
||||||
return withIOContext {
|
withIOContext {
|
||||||
authClient.newCall(
|
authClient
|
||||||
|
.newCall(
|
||||||
DELETE(
|
DELETE(
|
||||||
"${baseUrl}library-entries/${track.media_id}",
|
"${baseUrl}library-entries/${track.remoteId}",
|
||||||
headers = headersOf(
|
headers = headersOf(
|
||||||
"Content-Type",
|
"Content-Type",
|
||||||
"application/vnd.api+json",
|
"application/vnd.api+json",
|
||||||
@ -136,7 +138,6 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
track
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
suspend fun search(query: String): List<TrackSearch> {
|
suspend fun search(query: String): List<TrackSearch> {
|
||||||
@ -187,7 +188,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
suspend fun findLibManga(track: Track, userId: String): Track? {
|
suspend fun findLibManga(track: Track, userId: String): Track? {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val url = "${baseUrl}library-entries".toUri().buildUpon()
|
val url = "${baseUrl}library-entries".toUri().buildUpon()
|
||||||
.encodedQuery("filter[manga_id]=${track.media_id}&filter[user_id]=$userId")
|
.encodedQuery("filter[manga_id]=${track.remote_id}&filter[user_id]=$userId")
|
||||||
.appendQueryParameter("include", "manga")
|
.appendQueryParameter("include", "manga")
|
||||||
.build()
|
.build()
|
||||||
with(json) {
|
with(json) {
|
||||||
@ -210,7 +211,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
suspend fun getLibManga(track: Track): Track {
|
suspend fun getLibManga(track: Track): Track {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val url = "${baseUrl}library-entries".toUri().buildUpon()
|
val url = "${baseUrl}library-entries".toUri().buildUpon()
|
||||||
.encodedQuery("filter[id]=${track.media_id}")
|
.encodedQuery("filter[id]=${track.remote_id}")
|
||||||
.appendQueryParameter("include", "manga")
|
.appendQueryParameter("include", "manga")
|
||||||
.build()
|
.build()
|
||||||
with(json) {
|
with(json) {
|
||||||
|
@ -5,7 +5,7 @@ import okhttp3.Interceptor
|
|||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class KitsuInterceptor(val kitsu: Kitsu) : Interceptor {
|
class KitsuInterceptor(private val kitsu: Kitsu) : Interceptor {
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
@ -37,12 +37,12 @@ class KitsuSearchManga(obj: JsonObject) {
|
|||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
fun toTrack() = TrackSearch.create(TrackerManager.KITSU).apply {
|
fun toTrack() = TrackSearch.create(TrackerManager.KITSU).apply {
|
||||||
media_id = this@KitsuSearchManga.id
|
remote_id = this@KitsuSearchManga.id
|
||||||
title = canonicalTitle
|
title = canonicalTitle
|
||||||
total_chapters = chapterCount ?: 0
|
total_chapters = chapterCount ?: 0
|
||||||
cover_url = original ?: ""
|
cover_url = original ?: ""
|
||||||
summary = synopsis ?: ""
|
summary = synopsis ?: ""
|
||||||
tracking_url = KitsuApi.mangaUrl(media_id)
|
tracking_url = KitsuApi.mangaUrl(remote_id)
|
||||||
score = rating ?: -1f
|
score = rating ?: -1f
|
||||||
publishing_status = if (endDate == null) {
|
publishing_status = if (endDate == null) {
|
||||||
"Publishing"
|
"Publishing"
|
||||||
@ -70,12 +70,12 @@ class KitsuLibManga(obj: JsonObject, manga: JsonObject) {
|
|||||||
val progress = obj["attributes"]!!.jsonObject["progress"]!!.jsonPrimitive.int
|
val progress = obj["attributes"]!!.jsonObject["progress"]!!.jsonPrimitive.int
|
||||||
|
|
||||||
fun toTrack() = TrackSearch.create(TrackerManager.KITSU).apply {
|
fun toTrack() = TrackSearch.create(TrackerManager.KITSU).apply {
|
||||||
media_id = libraryId
|
remote_id = libraryId
|
||||||
title = canonicalTitle
|
title = canonicalTitle
|
||||||
total_chapters = chapterCount ?: 0
|
total_chapters = chapterCount ?: 0
|
||||||
cover_url = original
|
cover_url = original
|
||||||
summary = synopsis
|
summary = synopsis
|
||||||
tracking_url = KitsuApi.mangaUrl(media_id)
|
tracking_url = KitsuApi.mangaUrl(remote_id)
|
||||||
publishing_status = this@KitsuLibManga.status
|
publishing_status = this@KitsuLibManga.status
|
||||||
publishing_type = type
|
publishing_type = type
|
||||||
start_date = startDate
|
start_date = startDate
|
||||||
|
@ -52,7 +52,7 @@ class Komga(id: Long) : BaseTracker(id, "Komga"), EnhancedTracker {
|
|||||||
|
|
||||||
override fun getScoreList(): ImmutableList<String> = persistentListOf()
|
override fun getScoreList(): ImmutableList<String> = persistentListOf()
|
||||||
|
|
||||||
override fun displayScore(track: Track): String = ""
|
override fun displayScore(track: DomainTrack): String = ""
|
||||||
|
|
||||||
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
|
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
|
||||||
if (track.status != COMPLETED) {
|
if (track.status != COMPLETED) {
|
||||||
|
@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
|||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||||
|
|
||||||
class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker {
|
class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker {
|
||||||
|
|
||||||
@ -60,7 +61,7 @@ class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker
|
|||||||
|
|
||||||
override fun indexToScore(index: Int): Float = SCORE_LIST[index].toFloat()
|
override fun indexToScore(index: Int): Float = SCORE_LIST[index].toFloat()
|
||||||
|
|
||||||
override fun displayScore(track: Track): String = track.score.toString()
|
override fun displayScore(track: DomainTrack): String = track.score.toString()
|
||||||
|
|
||||||
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
|
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
|
||||||
if (track.status != COMPLETE_LIST && didReadChapter) {
|
if (track.status != COMPLETE_LIST && didReadChapter) {
|
||||||
@ -70,9 +71,8 @@ class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker
|
|||||||
return track
|
return track
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun delete(track: Track): Track {
|
override suspend fun delete(track: DomainTrack) {
|
||||||
api.deleteSeriesFromList(track)
|
api.deleteSeriesFromList(track)
|
||||||
return track
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
|
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
|
||||||
|
@ -30,6 +30,7 @@ import okhttp3.OkHttpClient
|
|||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||||
|
|
||||||
class MangaUpdatesApi(
|
class MangaUpdatesApi(
|
||||||
interceptor: MangaUpdatesInterceptor,
|
interceptor: MangaUpdatesInterceptor,
|
||||||
@ -48,7 +49,7 @@ class MangaUpdatesApi(
|
|||||||
|
|
||||||
suspend fun getSeriesListItem(track: Track): Pair<ListItem, Rating?> {
|
suspend fun getSeriesListItem(track: Track): Pair<ListItem, Rating?> {
|
||||||
val listItem = with(json) {
|
val listItem = with(json) {
|
||||||
authClient.newCall(GET("$baseUrl/v1/lists/series/${track.media_id}"))
|
authClient.newCall(GET("$baseUrl/v1/lists/series/${track.remote_id}"))
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<ListItem>()
|
.parseAs<ListItem>()
|
||||||
}
|
}
|
||||||
@ -63,7 +64,7 @@ class MangaUpdatesApi(
|
|||||||
val body = buildJsonArray {
|
val body = buildJsonArray {
|
||||||
addJsonObject {
|
addJsonObject {
|
||||||
putJsonObject("series") {
|
putJsonObject("series") {
|
||||||
put("id", track.media_id)
|
put("id", track.remote_id)
|
||||||
}
|
}
|
||||||
put("list_id", status)
|
put("list_id", status)
|
||||||
}
|
}
|
||||||
@ -87,7 +88,7 @@ class MangaUpdatesApi(
|
|||||||
val body = buildJsonArray {
|
val body = buildJsonArray {
|
||||||
addJsonObject {
|
addJsonObject {
|
||||||
putJsonObject("series") {
|
putJsonObject("series") {
|
||||||
put("id", track.media_id)
|
put("id", track.remote_id)
|
||||||
}
|
}
|
||||||
put("list_id", track.status)
|
put("list_id", track.status)
|
||||||
putJsonObject("status") {
|
putJsonObject("status") {
|
||||||
@ -106,9 +107,9 @@ class MangaUpdatesApi(
|
|||||||
updateSeriesRating(track)
|
updateSeriesRating(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun deleteSeriesFromList(track: Track) {
|
suspend fun deleteSeriesFromList(track: DomainTrack) {
|
||||||
val body = buildJsonArray {
|
val body = buildJsonArray {
|
||||||
add(track.media_id)
|
add(track.remoteId)
|
||||||
}
|
}
|
||||||
authClient.newCall(
|
authClient.newCall(
|
||||||
POST(
|
POST(
|
||||||
@ -122,7 +123,7 @@ class MangaUpdatesApi(
|
|||||||
private suspend fun getSeriesRating(track: Track): Rating? {
|
private suspend fun getSeriesRating(track: Track): Rating? {
|
||||||
return try {
|
return try {
|
||||||
with(json) {
|
with(json) {
|
||||||
authClient.newCall(GET("$baseUrl/v1/series/${track.media_id}/rating"))
|
authClient.newCall(GET("$baseUrl/v1/series/${track.remote_id}/rating"))
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<Rating>()
|
.parseAs<Rating>()
|
||||||
}
|
}
|
||||||
@ -138,7 +139,7 @@ class MangaUpdatesApi(
|
|||||||
}
|
}
|
||||||
authClient.newCall(
|
authClient.newCall(
|
||||||
PUT(
|
PUT(
|
||||||
url = "$baseUrl/v1/series/${track.media_id}/rating",
|
url = "$baseUrl/v1/series/${track.remote_id}/rating",
|
||||||
body = body.toString().toRequestBody(contentType),
|
body = body.toString().toRequestBody(contentType),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -146,7 +147,7 @@ class MangaUpdatesApi(
|
|||||||
} else {
|
} else {
|
||||||
authClient.newCall(
|
authClient.newCall(
|
||||||
DELETE(
|
DELETE(
|
||||||
url = "$baseUrl/v1/series/${track.media_id}/rating",
|
url = "$baseUrl/v1/series/${track.remote_id}/rating",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
|
@ -25,7 +25,7 @@ data class Record(
|
|||||||
|
|
||||||
fun Record.toTrackSearch(id: Long): TrackSearch {
|
fun Record.toTrackSearch(id: Long): TrackSearch {
|
||||||
return TrackSearch.create(id).apply {
|
return TrackSearch.create(id).apply {
|
||||||
media_id = this@toTrackSearch.seriesId ?: 0L
|
remote_id = this@toTrackSearch.seriesId ?: 0L
|
||||||
title = this@toTrackSearch.title?.htmlDecode() ?: ""
|
title = this@toTrackSearch.title?.htmlDecode() ?: ""
|
||||||
total_chapters = 0
|
total_chapters = 0
|
||||||
cover_url = this@toTrackSearch.image?.url?.original ?: ""
|
cover_url = this@toTrackSearch.image?.url?.original ?: ""
|
||||||
|
@ -8,9 +8,9 @@ class TrackSearch : Track {
|
|||||||
|
|
||||||
override var manga_id: Long = 0
|
override var manga_id: Long = 0
|
||||||
|
|
||||||
override var sync_id: Int = 0
|
override var tracker_id: Int = 0
|
||||||
|
|
||||||
override var media_id: Long = 0
|
override var remote_id: Long = 0
|
||||||
|
|
||||||
override var library_id: Long? = null
|
override var library_id: Long? = null
|
||||||
|
|
||||||
@ -47,22 +47,22 @@ class TrackSearch : Track {
|
|||||||
other as TrackSearch
|
other as TrackSearch
|
||||||
|
|
||||||
if (manga_id != other.manga_id) return false
|
if (manga_id != other.manga_id) return false
|
||||||
if (sync_id != other.sync_id) return false
|
if (tracker_id != other.tracker_id) return false
|
||||||
if (media_id != other.media_id) return false
|
if (remote_id != other.remote_id) return false
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = manga_id.hashCode()
|
var result = manga_id.hashCode()
|
||||||
result = 31 * result + sync_id
|
result = 31 * result + tracker_id
|
||||||
result = 31 * result + media_id.hashCode()
|
result = 31 * result + remote_id.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun create(serviceId: Long): TrackSearch = TrackSearch().apply {
|
fun create(serviceId: Long): TrackSearch = TrackSearch().apply {
|
||||||
sync_id = serviceId.toInt()
|
tracker_id = serviceId.toInt()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ import kotlinx.serialization.encodeToString
|
|||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||||
|
|
||||||
class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker {
|
class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker {
|
||||||
|
|
||||||
@ -65,7 +66,7 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker {
|
|||||||
|
|
||||||
override fun getScoreList(): ImmutableList<String> = SCORE_LIST
|
override fun getScoreList(): ImmutableList<String> = SCORE_LIST
|
||||||
|
|
||||||
override fun displayScore(track: Track): String {
|
override fun displayScore(track: DomainTrack): String {
|
||||||
return track.score.toInt().toString()
|
return track.score.toInt().toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,15 +92,15 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker {
|
|||||||
return api.updateItem(track)
|
return api.updateItem(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun delete(track: Track): Track {
|
override suspend fun delete(track: DomainTrack) {
|
||||||
return api.deleteItem(track)
|
api.deleteItem(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
|
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
|
||||||
val remoteTrack = api.findListItem(track)
|
val remoteTrack = api.findListItem(track)
|
||||||
return if (remoteTrack != null) {
|
return if (remoteTrack != null) {
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.media_id = remoteTrack.media_id
|
track.remote_id = remoteTrack.remote_id
|
||||||
|
|
||||||
if (track.status != COMPLETED) {
|
if (track.status != COMPLETED) {
|
||||||
val isRereading = track.status == REREADING
|
val isRereading = track.status == REREADING
|
||||||
|
@ -4,6 +4,7 @@ import android.net.Uri
|
|||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
|
import eu.kanade.tachiyomi.network.DELETE
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||||
@ -31,6 +32,7 @@ import tachiyomi.core.util.lang.withIOContext
|
|||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||||
|
|
||||||
class MyAnimeListApi(
|
class MyAnimeListApi(
|
||||||
private val trackId: Long,
|
private val trackId: Long,
|
||||||
@ -114,7 +116,7 @@ class MyAnimeListApi(
|
|||||||
.let {
|
.let {
|
||||||
val obj = it.jsonObject
|
val obj = it.jsonObject
|
||||||
TrackSearch.create(trackId).apply {
|
TrackSearch.create(trackId).apply {
|
||||||
media_id = obj["id"]!!.jsonPrimitive.long
|
remote_id = obj["id"]!!.jsonPrimitive.long
|
||||||
title = obj["title"]!!.jsonPrimitive.content
|
title = obj["title"]!!.jsonPrimitive.content
|
||||||
summary = obj["synopsis"]?.jsonPrimitive?.content ?: ""
|
summary = obj["synopsis"]?.jsonPrimitive?.content ?: ""
|
||||||
total_chapters = obj["num_chapters"]!!.jsonPrimitive.int
|
total_chapters = obj["num_chapters"]!!.jsonPrimitive.int
|
||||||
@ -122,7 +124,7 @@ class MyAnimeListApi(
|
|||||||
cover_url =
|
cover_url =
|
||||||
obj["main_picture"]?.jsonObject?.get("large")?.jsonPrimitive?.content
|
obj["main_picture"]?.jsonObject?.get("large")?.jsonPrimitive?.content
|
||||||
?: ""
|
?: ""
|
||||||
tracking_url = "https://myanimelist.net/manga/$media_id"
|
tracking_url = "https://myanimelist.net/manga/$remote_id"
|
||||||
publishing_status =
|
publishing_status =
|
||||||
obj["status"]!!.jsonPrimitive.content.replace("_", " ")
|
obj["status"]!!.jsonPrimitive.content.replace("_", " ")
|
||||||
publishing_type =
|
publishing_type =
|
||||||
@ -154,7 +156,7 @@ class MyAnimeListApi(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url(mangaUrl(track.media_id).toString())
|
.url(mangaUrl(track.remote_id).toString())
|
||||||
.put(formBodyBuilder.build())
|
.put(formBodyBuilder.build())
|
||||||
.build()
|
.build()
|
||||||
with(json) {
|
with(json) {
|
||||||
@ -166,24 +168,18 @@ class MyAnimeListApi(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun deleteItem(track: Track): Track {
|
suspend fun deleteItem(track: DomainTrack) {
|
||||||
return withIOContext {
|
withIOContext {
|
||||||
val request = Request.Builder()
|
authClient
|
||||||
.url(mangaUrl(track.media_id).toString())
|
.newCall(DELETE(mangaUrl(track.remoteId).toString()))
|
||||||
.delete()
|
|
||||||
.build()
|
|
||||||
with(json) {
|
|
||||||
authClient.newCall(request)
|
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
track
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun findListItem(track: Track): Track? {
|
suspend fun findListItem(track: Track): Track? {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val uri = "$baseApiUrl/manga".toUri().buildUpon()
|
val uri = "$baseApiUrl/manga".toUri().buildUpon()
|
||||||
.appendPath(track.media_id.toString())
|
.appendPath(track.remote_id.toString())
|
||||||
.appendQueryParameter("fields", "num_chapters,my_list_status{start_date,finish_date}")
|
.appendQueryParameter("fields", "num_chapters,my_list_status{start_date,finish_date}")
|
||||||
.build()
|
.build()
|
||||||
with(json) {
|
with(json) {
|
||||||
|
@ -13,6 +13,7 @@ import kotlinx.serialization.encodeToString
|
|||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||||
|
|
||||||
class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker {
|
class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker {
|
||||||
|
|
||||||
@ -37,7 +38,7 @@ class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker {
|
|||||||
|
|
||||||
override fun getScoreList(): ImmutableList<String> = SCORE_LIST
|
override fun getScoreList(): ImmutableList<String> = SCORE_LIST
|
||||||
|
|
||||||
override fun displayScore(track: Track): String {
|
override fun displayScore(track: DomainTrack): String {
|
||||||
return track.score.toInt().toString()
|
return track.score.toInt().toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,8 +60,8 @@ class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker {
|
|||||||
return api.updateLibManga(track, getUsername())
|
return api.updateLibManga(track, getUsername())
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun delete(track: Track): Track {
|
override suspend fun delete(track: DomainTrack) {
|
||||||
return api.deleteLibManga(track)
|
api.deleteLibManga(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
|
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
|
||||||
|
@ -27,6 +27,7 @@ import okhttp3.OkHttpClient
|
|||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import tachiyomi.core.util.lang.withIOContext
|
import tachiyomi.core.util.lang.withIOContext
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||||
|
|
||||||
class ShikimoriApi(
|
class ShikimoriApi(
|
||||||
private val trackId: Long,
|
private val trackId: Long,
|
||||||
@ -44,7 +45,7 @@ class ShikimoriApi(
|
|||||||
val payload = buildJsonObject {
|
val payload = buildJsonObject {
|
||||||
putJsonObject("user_rate") {
|
putJsonObject("user_rate") {
|
||||||
put("user_id", userId)
|
put("user_id", userId)
|
||||||
put("target_id", track.media_id)
|
put("target_id", track.remote_id)
|
||||||
put("target_type", "Manga")
|
put("target_type", "Manga")
|
||||||
put("chapters", track.last_chapter_read.toInt())
|
put("chapters", track.last_chapter_read.toInt())
|
||||||
put("score", track.score.toInt())
|
put("score", track.score.toInt())
|
||||||
@ -69,14 +70,11 @@ class ShikimoriApi(
|
|||||||
|
|
||||||
suspend fun updateLibManga(track: Track, userId: String): Track = addLibManga(track, userId)
|
suspend fun updateLibManga(track: Track, userId: String): Track = addLibManga(track, userId)
|
||||||
|
|
||||||
suspend fun deleteLibManga(track: Track): Track {
|
suspend fun deleteLibManga(track: DomainTrack) {
|
||||||
return withIOContext {
|
withIOContext {
|
||||||
authClient.newCall(
|
authClient
|
||||||
DELETE(
|
.newCall(DELETE("$apiUrl/v2/user_rates/${track.libraryId}"))
|
||||||
"$apiUrl/v2/user_rates/${track.library_id}",
|
.awaitSuccess()
|
||||||
),
|
|
||||||
).awaitSuccess()
|
|
||||||
track
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,7 +100,7 @@ class ShikimoriApi(
|
|||||||
|
|
||||||
private fun jsonToSearch(obj: JsonObject): TrackSearch {
|
private fun jsonToSearch(obj: JsonObject): TrackSearch {
|
||||||
return TrackSearch.create(trackId).apply {
|
return TrackSearch.create(trackId).apply {
|
||||||
media_id = obj["id"]!!.jsonPrimitive.long
|
remote_id = obj["id"]!!.jsonPrimitive.long
|
||||||
title = obj["name"]!!.jsonPrimitive.content
|
title = obj["name"]!!.jsonPrimitive.content
|
||||||
total_chapters = obj["chapters"]!!.jsonPrimitive.int
|
total_chapters = obj["chapters"]!!.jsonPrimitive.int
|
||||||
cover_url = baseUrl + obj["image"]!!.jsonObject["preview"]!!.jsonPrimitive.content
|
cover_url = baseUrl + obj["image"]!!.jsonObject["preview"]!!.jsonPrimitive.content
|
||||||
@ -118,7 +116,7 @@ class ShikimoriApi(
|
|||||||
private fun jsonToTrack(obj: JsonObject, mangas: JsonObject): Track {
|
private fun jsonToTrack(obj: JsonObject, mangas: JsonObject): Track {
|
||||||
return Track.create(trackId).apply {
|
return Track.create(trackId).apply {
|
||||||
title = mangas["name"]!!.jsonPrimitive.content
|
title = mangas["name"]!!.jsonPrimitive.content
|
||||||
media_id = obj["id"]!!.jsonPrimitive.long
|
remote_id = obj["id"]!!.jsonPrimitive.long
|
||||||
total_chapters = mangas["chapters"]!!.jsonPrimitive.int
|
total_chapters = mangas["chapters"]!!.jsonPrimitive.int
|
||||||
library_id = obj["id"]!!.jsonPrimitive.long
|
library_id = obj["id"]!!.jsonPrimitive.long
|
||||||
last_chapter_read = obj["chapters"]!!.jsonPrimitive.float
|
last_chapter_read = obj["chapters"]!!.jsonPrimitive.float
|
||||||
@ -131,7 +129,7 @@ class ShikimoriApi(
|
|||||||
suspend fun findLibManga(track: Track, userId: String): Track? {
|
suspend fun findLibManga(track: Track, userId: String): Track? {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val urlMangas = "$apiUrl/mangas".toUri().buildUpon()
|
val urlMangas = "$apiUrl/mangas".toUri().buildUpon()
|
||||||
.appendPath(track.media_id.toString())
|
.appendPath(track.remote_id.toString())
|
||||||
.build()
|
.build()
|
||||||
val mangas = with(json) {
|
val mangas = with(json) {
|
||||||
authClient.newCall(GET(urlMangas.toString()))
|
authClient.newCall(GET(urlMangas.toString()))
|
||||||
@ -141,7 +139,7 @@ class ShikimoriApi(
|
|||||||
|
|
||||||
val url = "$apiUrl/v2/user_rates".toUri().buildUpon()
|
val url = "$apiUrl/v2/user_rates".toUri().buildUpon()
|
||||||
.appendQueryParameter("user_id", userId)
|
.appendQueryParameter("user_id", userId)
|
||||||
.appendQueryParameter("target_id", track.media_id.toString())
|
.appendQueryParameter("target_id", track.remote_id.toString())
|
||||||
.appendQueryParameter("target_type", "Manga")
|
.appendQueryParameter("target_type", "Manga")
|
||||||
.build()
|
.build()
|
||||||
with(json) {
|
with(json) {
|
||||||
|
@ -5,7 +5,7 @@ import okhttp3.Interceptor
|
|||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class ShikimoriInterceptor(val shikimori: Shikimori) : Interceptor {
|
class ShikimoriInterceptor(private val shikimori: Shikimori) : Interceptor {
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ class Suwayomi(id: Long) : BaseTracker(id, "Suwayomi"), EnhancedTracker {
|
|||||||
|
|
||||||
override fun getScoreList(): ImmutableList<String> = persistentListOf()
|
override fun getScoreList(): ImmutableList<String> = persistentListOf()
|
||||||
|
|
||||||
override fun displayScore(track: Track): String = ""
|
override fun displayScore(track: DomainTrack): String = ""
|
||||||
|
|
||||||
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
|
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
|
||||||
if (track.status != COMPLETED) {
|
if (track.status != COMPLETED) {
|
||||||
|
@ -23,6 +23,7 @@ import eu.kanade.tachiyomi.network.NetworkHelper
|
|||||||
import eu.kanade.tachiyomi.source.AndroidSourceManager
|
import eu.kanade.tachiyomi.source.AndroidSourceManager
|
||||||
import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
|
import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
import nl.adaptivity.xmlutil.XmlDeclMode
|
import nl.adaptivity.xmlutil.XmlDeclMode
|
||||||
import nl.adaptivity.xmlutil.core.XmlVersion
|
import nl.adaptivity.xmlutil.core.XmlVersion
|
||||||
import nl.adaptivity.xmlutil.serialization.XML
|
import nl.adaptivity.xmlutil.serialization.XML
|
||||||
@ -107,8 +108,11 @@ class AppModule(val app: Application) : InjektModule {
|
|||||||
xmlVersion = XmlVersion.XML10
|
xmlVersion = XmlVersion.XML10
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
addSingletonFactory<ProtoBuf> {
|
||||||
|
ProtoBuf
|
||||||
|
}
|
||||||
|
|
||||||
addSingletonFactory { ChapterCache(app) }
|
addSingletonFactory { ChapterCache(app, get()) }
|
||||||
addSingletonFactory { CoverCache(app) }
|
addSingletonFactory { CoverCache(app) }
|
||||||
|
|
||||||
addSingletonFactory { NetworkHelper(app, get()) }
|
addSingletonFactory { NetworkHelper(app, get()) }
|
||||||
|
@ -141,6 +141,11 @@ internal object ExtensionLoader {
|
|||||||
?.asSequence()
|
?.asSequence()
|
||||||
?.filter { it.isFile && it.extension == PRIVATE_EXTENSION_EXTENSION }
|
?.filter { it.isFile && it.extension == PRIVATE_EXTENSION_EXTENSION }
|
||||||
?.mapNotNull {
|
?.mapNotNull {
|
||||||
|
// Just in case, since Android 14+ requires them to be read-only
|
||||||
|
if (it.canWrite()) {
|
||||||
|
it.setReadOnly()
|
||||||
|
}
|
||||||
|
|
||||||
val path = it.absolutePath
|
val path = it.absolutePath
|
||||||
pkgManager.getPackageArchiveInfo(path, PACKAGE_FLAGS)
|
pkgManager.getPackageArchiveInfo(path, PACKAGE_FLAGS)
|
||||||
?.apply { applicationInfo.fixBasePaths(path) }
|
?.apply { applicationInfo.fixBasePaths(path) }
|
||||||
@ -277,7 +282,12 @@ internal object ExtensionLoader {
|
|||||||
val hasReadme = appInfo.metaData.getInt(METADATA_HAS_README, 0) == 1
|
val hasReadme = appInfo.metaData.getInt(METADATA_HAS_README, 0) == 1
|
||||||
val hasChangelog = appInfo.metaData.getInt(METADATA_HAS_CHANGELOG, 0) == 1
|
val hasChangelog = appInfo.metaData.getInt(METADATA_HAS_CHANGELOG, 0) == 1
|
||||||
|
|
||||||
val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
|
val classLoader = try {
|
||||||
|
PathClassLoader(appInfo.sourceDir, null, context.classLoader)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, e) { "Extension load error: $extName ($pkgName)" }
|
||||||
|
return LoadResult.Error
|
||||||
|
}
|
||||||
|
|
||||||
val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!!
|
val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!!
|
||||||
.split(";")
|
.split(";")
|
||||||
|
@ -217,7 +217,7 @@ class LibraryScreenModel(
|
|||||||
if (isNotLoggedInAnyTrack || trackFiltersIsIgnored) return@tracking true
|
if (isNotLoggedInAnyTrack || trackFiltersIsIgnored) return@tracking true
|
||||||
|
|
||||||
val mangaTracks = trackMap
|
val mangaTracks = trackMap
|
||||||
.mapValues { entry -> entry.value.map { it.syncId } }[item.libraryManga.id]
|
.mapValues { entry -> entry.value.map { it.trackerId } }[item.libraryManga.id]
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
|
|
||||||
val isExcluded = excludedTracks.isNotEmpty() && mangaTracks.fastAny { it in excludedTracks }
|
val isExcluded = excludedTracks.isNotEmpty() && mangaTracks.fastAny { it in excludedTracks }
|
||||||
@ -257,7 +257,7 @@ class LibraryScreenModel(
|
|||||||
entry.value.isEmpty() -> null
|
entry.value.isEmpty() -> null
|
||||||
else ->
|
else ->
|
||||||
entry.value
|
entry.value
|
||||||
.mapNotNull { trackerMap[it.syncId]?.get10PointScore(it) }
|
.mapNotNull { trackerMap[it.trackerId]?.get10PointScore(it) }
|
||||||
.average()
|
.average()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -983,7 +983,7 @@ class MangaScreenModel(
|
|||||||
.map { tracks ->
|
.map { tracks ->
|
||||||
loggedInTrackers
|
loggedInTrackers
|
||||||
// Map to TrackItem
|
// Map to TrackItem
|
||||||
.map { service -> TrackItem(tracks.find { it.syncId == service.id }, service) }
|
.map { service -> TrackItem(tracks.find { it.trackerId == service.id }, service) }
|
||||||
// Show only if the service supports this manga's source
|
// Show only if the service supports this manga's source
|
||||||
.filter { (it.tracker as? EnhancedTracker)?.accept(source!!) ?: true }
|
.filter { (it.tracker as? EnhancedTracker)?.accept(source!!) ?: true }
|
||||||
}
|
}
|
||||||
|
@ -244,7 +244,7 @@ data class TrackInfoDialogHomeScreen(
|
|||||||
val source = Injekt.get<SourceManager>().getOrStub(sourceId)
|
val source = Injekt.get<SourceManager>().getOrStub(sourceId)
|
||||||
return loggedInTrackers
|
return loggedInTrackers
|
||||||
// Map to TrackItem
|
// Map to TrackItem
|
||||||
.map { service -> TrackItem(find { it.syncId == service.id }, service) }
|
.map { service -> TrackItem(find { it.trackerId == service.id }, service) }
|
||||||
// Show only if the service supports this manga's source
|
// Show only if the service supports this manga's source
|
||||||
.filter { (it.tracker as? EnhancedTracker)?.accept(source) ?: true }
|
.filter { (it.tracker as? EnhancedTracker)?.accept(source) ?: true }
|
||||||
}
|
}
|
||||||
@ -399,7 +399,7 @@ private data class TrackScoreSelectorScreen(
|
|||||||
private class Model(
|
private class Model(
|
||||||
private val track: Track,
|
private val track: Track,
|
||||||
private val tracker: Tracker,
|
private val tracker: Tracker,
|
||||||
) : StateScreenModel<Model.State>(State(tracker.displayScore(track.toDbTrack()))) {
|
) : StateScreenModel<Model.State>(State(tracker.displayScore(track))) {
|
||||||
|
|
||||||
fun getSelections(): ImmutableList<String> {
|
fun getSelections(): ImmutableList<String> {
|
||||||
return tracker.getScoreList()
|
return tracker.getScoreList()
|
||||||
@ -816,7 +816,7 @@ private data class TrackerRemoveScreen(
|
|||||||
|
|
||||||
fun deleteMangaFromService() {
|
fun deleteMangaFromService() {
|
||||||
screenModelScope.launchNonCancellable {
|
screenModelScope.launchNonCancellable {
|
||||||
(tracker as DeletableTracker).delete(track.toDbTrack())
|
(tracker as DeletableTracker).delete(track)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user