mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-21 12:17:12 +03:00
parent
c07cc8dc21
commit
76df725cab
63 changed files with 2030 additions and 1887 deletions
|
@ -248,7 +248,6 @@ dependencies {
|
|||
implementation(libs.aboutLibraries.compose)
|
||||
implementation(libs.bundles.voyager)
|
||||
implementation(libs.compose.materialmotion)
|
||||
implementation(libs.compose.simpleicons)
|
||||
implementation(libs.swipe)
|
||||
|
||||
// Logging
|
||||
|
|
|
@ -17,31 +17,29 @@ class AddAnimeTracks(
|
|||
private val syncChapterProgressWithTrack: SyncEpisodeProgressWithTrack,
|
||||
) {
|
||||
|
||||
suspend fun bindEnhancedTracks(anime: Anime, source: AnimeSource) {
|
||||
withNonCancellableContext {
|
||||
getTracks.await(anime.id)
|
||||
.filterIsInstance<EnhancedAnimeTracker>()
|
||||
.filter { it.accept(source) }
|
||||
.forEach { service ->
|
||||
try {
|
||||
service.match(anime)?.let { track ->
|
||||
track.anime_id = anime.id
|
||||
(service as Tracker).animeService.bind(track)
|
||||
insertTrack.await(track.toDomainTrack()!!)
|
||||
suspend fun bindEnhancedTracks(anime: Anime, source: AnimeSource) = withNonCancellableContext {
|
||||
getTracks.await(anime.id)
|
||||
.filterIsInstance<EnhancedAnimeTracker>()
|
||||
.filter { it.accept(source) }
|
||||
.forEach { service ->
|
||||
try {
|
||||
service.match(anime)?.let { track ->
|
||||
track.anime_id = anime.id
|
||||
(service as Tracker).animeService.bind(track)
|
||||
insertTrack.await(track.toDomainTrack()!!)
|
||||
|
||||
syncChapterProgressWithTrack.await(
|
||||
anime.id,
|
||||
track.toDomainTrack()!!,
|
||||
service.animeService,
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logcat(
|
||||
LogPriority.WARN,
|
||||
e,
|
||||
) { "Could not match manga: ${anime.title} with service $service" }
|
||||
syncChapterProgressWithTrack.await(
|
||||
anime.id,
|
||||
track.toDomainTrack()!!,
|
||||
service.animeService,
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logcat(
|
||||
LogPriority.WARN,
|
||||
e,
|
||||
) { "Could not match anime: ${anime.title} with service $service" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,31 +17,29 @@ class AddMangaTracks(
|
|||
private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack,
|
||||
) {
|
||||
|
||||
suspend fun bindEnhancedTracks(manga: Manga, source: MangaSource) {
|
||||
withNonCancellableContext {
|
||||
getTracks.await(manga.id)
|
||||
.filterIsInstance<EnhancedMangaTracker>()
|
||||
.filter { it.accept(source) }
|
||||
.forEach { service ->
|
||||
try {
|
||||
service.match(manga)?.let { track ->
|
||||
track.manga_id = manga.id
|
||||
(service as Tracker).mangaService.bind(track)
|
||||
insertTrack.await(track.toDomainTrack()!!)
|
||||
suspend fun bindEnhancedTracks(manga: Manga, source: MangaSource) = withNonCancellableContext {
|
||||
getTracks.await(manga.id)
|
||||
.filterIsInstance<EnhancedMangaTracker>()
|
||||
.filter { it.accept(source) }
|
||||
.forEach { service ->
|
||||
try {
|
||||
service.match(manga)?.let { track ->
|
||||
track.manga_id = manga.id
|
||||
(service as Tracker).mangaService.bind(track)
|
||||
insertTrack.await(track.toDomainTrack()!!)
|
||||
|
||||
syncChapterProgressWithTrack.await(
|
||||
manga.id,
|
||||
track.toDomainTrack()!!,
|
||||
service.mangaService,
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logcat(
|
||||
LogPriority.WARN,
|
||||
e,
|
||||
) { "Could not match manga: ${manga.title} with service $service" }
|
||||
syncChapterProgressWithTrack.await(
|
||||
manga.id,
|
||||
track.toDomainTrack()!!,
|
||||
service.mangaService,
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logcat(
|
||||
LogPriority.WARN,
|
||||
e,
|
||||
) { "Could not match manga: ${manga.title} with service $service" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,35 @@ import kotlinx.coroutines.delay
|
|||
import tachiyomi.domain.category.model.Category
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Composable
|
||||
fun CategorySortAlphabeticallyDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onSort: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onSort()
|
||||
onDismissRequest()
|
||||
}) {
|
||||
Text(text = stringResource(R.string.action_ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(R.string.action_cancel))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(text = stringResource(R.string.action_sort_category))
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(R.string.sort_category_confirmation))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CategoryCreateDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
|
|
|
@ -302,8 +302,14 @@ private fun AnimeScreenSmallImpl(
|
|||
|
||||
val episodes = remember(state) { state.processedEpisodes }
|
||||
|
||||
val isAnySelected by remember {
|
||||
derivedStateOf {
|
||||
episodes.fastAny { it.selected }
|
||||
}
|
||||
}
|
||||
|
||||
val internalOnBackPressed = {
|
||||
if (episodes.fastAny { it.selected }) {
|
||||
if (isAnySelected) {
|
||||
onAllEpisodeSelected(false)
|
||||
} else {
|
||||
onBackClicked()
|
||||
|
@ -312,17 +318,22 @@ private fun AnimeScreenSmallImpl(
|
|||
BackHandler(onBack = internalOnBackPressed)
|
||||
Scaffold(
|
||||
topBar = {
|
||||
val firstVisibleItemIndex by remember {
|
||||
derivedStateOf { episodeListState.firstVisibleItemIndex }
|
||||
val selectedEpisodeCount: Int = remember(episodes) {
|
||||
episodes.count { it.selected }
|
||||
}
|
||||
val firstVisibleItemScrollOffset by remember {
|
||||
derivedStateOf { episodeListState.firstVisibleItemScrollOffset }
|
||||
val isFirstItemVisible by remember {
|
||||
derivedStateOf { episodeListState.firstVisibleItemIndex == 0 }
|
||||
}
|
||||
val isFirstItemScrolled by remember {
|
||||
derivedStateOf { episodeListState.firstVisibleItemScrollOffset > 0 }
|
||||
}
|
||||
val animatedTitleAlpha by animateFloatAsState(
|
||||
if (firstVisibleItemIndex > 0) 1f else 0f,
|
||||
if (!isFirstItemVisible) 1f else 0f,
|
||||
label = "Top Bar Title",
|
||||
)
|
||||
val animatedBgAlpha by animateFloatAsState(
|
||||
if (firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0) 1f else 0f,
|
||||
if (!isFirstItemVisible || isFirstItemScrolled) 1f else 0f,
|
||||
label = "Top Bar Background",
|
||||
)
|
||||
EntryToolbar(
|
||||
title = state.anime.title,
|
||||
|
@ -337,7 +348,7 @@ private fun AnimeScreenSmallImpl(
|
|||
onClickRefresh = onRefresh,
|
||||
onClickMigrate = onMigrateClicked,
|
||||
changeAnimeSkipIntro = changeAnimeSkipIntro,
|
||||
actionModeCounter = episodes.count { it.selected },
|
||||
actionModeCounter = selectedEpisodeCount,
|
||||
onSelectAll = { onAllEpisodeSelected(true) },
|
||||
onInvertSelection = { onInvertSelection() },
|
||||
isManga = false,
|
||||
|
@ -345,8 +356,11 @@ private fun AnimeScreenSmallImpl(
|
|||
)
|
||||
},
|
||||
bottomBar = {
|
||||
val selectedEpisodes = remember(episodes) {
|
||||
episodes.filter { it.selected }
|
||||
}
|
||||
SharedAnimeBottomActionMenu(
|
||||
selected = episodes.filter { it.selected },
|
||||
selected = selectedEpisodes,
|
||||
onEpisodeClicked = onEpisodeClicked,
|
||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||
onMultiMarkAsSeenClicked = onMultiMarkAsSeenClicked,
|
||||
|
@ -359,19 +373,20 @@ private fun AnimeScreenSmallImpl(
|
|||
},
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||
floatingActionButton = {
|
||||
val isFABVisible = remember(episodes) {
|
||||
episodes.fastAny { !it.episode.seen } && !isAnySelected
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = episodes.fastAny { !it.episode.seen } && episodes.fastAll { !it.selected },
|
||||
visible = isFABVisible,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
ExtendedFloatingActionButton(
|
||||
text = {
|
||||
val id = if (state.episodes.fastAny { it.episode.seen }) {
|
||||
R.string.action_resume
|
||||
} else {
|
||||
R.string.action_start
|
||||
val isWatching = remember(state.episodes) {
|
||||
state.episodes.fastAny { it.episode.seen }
|
||||
}
|
||||
Text(text = stringResource(id))
|
||||
Text(text = stringResource(if (isWatching) R.string.action_resume else R.string.action_start))
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
|
@ -390,7 +405,7 @@ private fun AnimeScreenSmallImpl(
|
|||
PullRefresh(
|
||||
refreshing = state.isRefreshingData,
|
||||
onRefresh = onRefresh,
|
||||
enabled = episodes.fastAll { !it.selected },
|
||||
enabled = !isAnySelected,
|
||||
indicatorPadding = WindowInsets.systemBars.only(WindowInsetsSides.Top).asPaddingValues(),
|
||||
) {
|
||||
val layoutDirection = LocalLayoutDirection.current
|
||||
|
@ -462,10 +477,13 @@ private fun AnimeScreenSmallImpl(
|
|||
key = EntryScreenItem.ITEM_HEADER,
|
||||
contentType = EntryScreenItem.ITEM_HEADER,
|
||||
) {
|
||||
val missingItemsCount = remember(episodes) {
|
||||
episodes.map { it.episode.episodeNumber }.missingItemsCount()
|
||||
}
|
||||
ItemHeader(
|
||||
enabled = episodes.fastAll { !it.selected },
|
||||
enabled = !isAnySelected,
|
||||
itemCount = episodes.size,
|
||||
missingItemsCount = episodes.map { it.episode.episodeNumber }.missingItemsCount(),
|
||||
missingItemsCount = missingItemsCount,
|
||||
onClick = onFilterClicked,
|
||||
isManga = false,
|
||||
)
|
||||
|
@ -574,13 +592,19 @@ fun AnimeScreenLargeImpl(
|
|||
|
||||
val episodes = remember(state) { state.processedEpisodes }
|
||||
|
||||
val isAnySelected by remember {
|
||||
derivedStateOf {
|
||||
episodes.fastAny { it.selected }
|
||||
}
|
||||
}
|
||||
|
||||
val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
|
||||
var topBarHeight by remember { mutableIntStateOf(0) }
|
||||
|
||||
PullRefresh(
|
||||
refreshing = state.isRefreshingData,
|
||||
onRefresh = onRefresh,
|
||||
enabled = episodes.fastAll { !it.selected },
|
||||
enabled = !isAnySelected,
|
||||
indicatorPadding = PaddingValues(
|
||||
start = insetPadding.calculateStartPadding(layoutDirection),
|
||||
top = with(density) { topBarHeight.toDp() },
|
||||
|
@ -590,7 +614,7 @@ fun AnimeScreenLargeImpl(
|
|||
val episodeListState = rememberLazyListState()
|
||||
|
||||
val internalOnBackPressed = {
|
||||
if (episodes.fastAny { it.selected }) {
|
||||
if (isAnySelected) {
|
||||
onAllEpisodeSelected(false)
|
||||
} else {
|
||||
onBackClicked()
|
||||
|
@ -600,10 +624,13 @@ fun AnimeScreenLargeImpl(
|
|||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
val selectedEpisodeCount = remember(episodes) {
|
||||
episodes.count { it.selected }
|
||||
}
|
||||
EntryToolbar(
|
||||
modifier = Modifier.onSizeChanged { topBarHeight = (it.height) },
|
||||
title = state.anime.title,
|
||||
titleAlphaProvider = { if (episodes.fastAny { it.selected }) 1f else 0f },
|
||||
titleAlphaProvider = { if (isAnySelected) 1f else 0f },
|
||||
backgroundAlphaProvider = { 1f },
|
||||
hasFilters = state.anime.episodesFiltered(),
|
||||
onBackClicked = internalOnBackPressed,
|
||||
|
@ -614,7 +641,7 @@ fun AnimeScreenLargeImpl(
|
|||
onClickRefresh = onRefresh,
|
||||
onClickMigrate = onMigrateClicked,
|
||||
changeAnimeSkipIntro = changeAnimeSkipIntro,
|
||||
actionModeCounter = episodes.count { it.selected },
|
||||
actionModeCounter = selectedEpisodeCount,
|
||||
onSelectAll = { onAllEpisodeSelected(true) },
|
||||
onInvertSelection = { onInvertSelection() },
|
||||
isManga = false,
|
||||
|
@ -626,8 +653,11 @@ fun AnimeScreenLargeImpl(
|
|||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.BottomEnd,
|
||||
) {
|
||||
val selectedEpisodes = remember(episodes) {
|
||||
episodes.filter { it.selected }
|
||||
}
|
||||
SharedAnimeBottomActionMenu(
|
||||
selected = episodes.filter { it.selected },
|
||||
selected = selectedEpisodes,
|
||||
onEpisodeClicked = onEpisodeClicked,
|
||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||
onMultiMarkAsSeenClicked = onMultiMarkAsSeenClicked,
|
||||
|
@ -641,19 +671,20 @@ fun AnimeScreenLargeImpl(
|
|||
},
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||
floatingActionButton = {
|
||||
val isFABVisible = remember(episodes) {
|
||||
episodes.fastAny { !it.episode.seen } && !isAnySelected
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = episodes.fastAny { !it.episode.seen } && episodes.fastAll { !it.selected },
|
||||
visible = isFABVisible,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
ExtendedFloatingActionButton(
|
||||
text = {
|
||||
val id = if (state.episodes.fastAny { it.episode.seen }) {
|
||||
R.string.action_resume
|
||||
} else {
|
||||
R.string.action_start
|
||||
val isWatching = remember(state.episodes) {
|
||||
state.episodes.fastAny { it.episode.seen }
|
||||
}
|
||||
Text(text = stringResource(id))
|
||||
Text(text = stringResource(if (isWatching) R.string.action_resume else R.string.action_start))
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
|
@ -729,10 +760,13 @@ fun AnimeScreenLargeImpl(
|
|||
key = EntryScreenItem.ITEM_HEADER,
|
||||
contentType = EntryScreenItem.ITEM_HEADER,
|
||||
) {
|
||||
val missingItemsCount = remember(episodes) {
|
||||
episodes.map { it.episode.episodeNumber }.missingItemsCount()
|
||||
}
|
||||
ItemHeader(
|
||||
enabled = episodes.fastAll { !it.selected },
|
||||
enabled = !isAnySelected,
|
||||
itemCount = episodes.size,
|
||||
missingItemsCount = episodes.map { it.episode.episodeNumber }.missingItemsCount(),
|
||||
missingItemsCount = missingItemsCount,
|
||||
onClick = onFilterButtonClicked,
|
||||
isManga = false,
|
||||
)
|
||||
|
|
|
@ -298,7 +298,7 @@ fun ExpandableAnimeDescription(
|
|||
) {
|
||||
tags.forEach {
|
||||
TagsChip(
|
||||
modifier = Modifier.padding(vertical = 4.dp),
|
||||
modifier = DefaultTagChipModifier,
|
||||
text = it,
|
||||
onClick = {
|
||||
tagSelected = it
|
||||
|
@ -314,7 +314,7 @@ fun ExpandableAnimeDescription(
|
|||
) {
|
||||
items(items = tags) {
|
||||
TagsChip(
|
||||
modifier = Modifier.padding(vertical = 4.dp),
|
||||
modifier = DefaultTagChipModifier,
|
||||
text = it,
|
||||
onClick = {
|
||||
tagSelected = it
|
||||
|
@ -676,6 +676,8 @@ private fun AnimeSummary(
|
|||
}
|
||||
}
|
||||
|
||||
private val DefaultTagChipModifier = Modifier.padding(vertical = 4.dp)
|
||||
|
||||
@Composable
|
||||
private fun TagsChip(
|
||||
text: String,
|
||||
|
|
|
@ -282,8 +282,14 @@ private fun MangaScreenSmallImpl(
|
|||
|
||||
val chapters = remember(state) { state.processedChapters }
|
||||
|
||||
val isAnySelected by remember {
|
||||
derivedStateOf {
|
||||
chapters.fastAny { it.selected }
|
||||
}
|
||||
}
|
||||
|
||||
val internalOnBackPressed = {
|
||||
if (chapters.fastAny { it.selected }) {
|
||||
if (isAnySelected) {
|
||||
onAllChapterSelected(false)
|
||||
} else {
|
||||
onBackClicked()
|
||||
|
@ -293,17 +299,22 @@ private fun MangaScreenSmallImpl(
|
|||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
val firstVisibleItemIndex by remember {
|
||||
derivedStateOf { chapterListState.firstVisibleItemIndex }
|
||||
val selectedChapterCount: Int = remember(chapters) {
|
||||
chapters.count { it.selected }
|
||||
}
|
||||
val firstVisibleItemScrollOffset by remember {
|
||||
derivedStateOf { chapterListState.firstVisibleItemScrollOffset }
|
||||
val isFirstItemVisible by remember {
|
||||
derivedStateOf { chapterListState.firstVisibleItemIndex == 0 }
|
||||
}
|
||||
val isFirstItemScrolled by remember {
|
||||
derivedStateOf { chapterListState.firstVisibleItemScrollOffset > 0 }
|
||||
}
|
||||
val animatedTitleAlpha by animateFloatAsState(
|
||||
if (firstVisibleItemIndex > 0) 1f else 0f,
|
||||
if (!isFirstItemVisible) 1f else 0f,
|
||||
label = "Top Bar Title",
|
||||
)
|
||||
val animatedBgAlpha by animateFloatAsState(
|
||||
if (firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0) 1f else 0f,
|
||||
if (!isFirstItemVisible || isFirstItemScrolled) 1f else 0f,
|
||||
label = "Top Bar Background",
|
||||
)
|
||||
EntryToolbar(
|
||||
title = state.manga.title,
|
||||
|
@ -318,7 +329,7 @@ private fun MangaScreenSmallImpl(
|
|||
onClickRefresh = onRefresh,
|
||||
onClickMigrate = onMigrateClicked,
|
||||
changeAnimeSkipIntro = null,
|
||||
actionModeCounter = chapters.count { it.selected },
|
||||
actionModeCounter = selectedChapterCount,
|
||||
onSelectAll = { onAllChapterSelected(true) },
|
||||
onInvertSelection = { onInvertSelection() },
|
||||
isManga = true,
|
||||
|
@ -326,8 +337,11 @@ private fun MangaScreenSmallImpl(
|
|||
)
|
||||
},
|
||||
bottomBar = {
|
||||
val selectedChapters = remember(chapters) {
|
||||
chapters.filter { it.selected }
|
||||
}
|
||||
SharedMangaBottomActionMenu(
|
||||
selected = chapters.filter { it.selected },
|
||||
selected = selectedChapters,
|
||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
||||
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
|
||||
|
@ -338,19 +352,20 @@ private fun MangaScreenSmallImpl(
|
|||
},
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||
floatingActionButton = {
|
||||
val isFABVisible = remember(chapters) {
|
||||
chapters.fastAny { !it.chapter.read } && !isAnySelected
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = chapters.fastAny { !it.chapter.read } && chapters.fastAll { !it.selected },
|
||||
visible = isFABVisible,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
ExtendedFloatingActionButton(
|
||||
text = {
|
||||
val id = if (state.chapters.fastAny { it.chapter.read }) {
|
||||
R.string.action_resume
|
||||
} else {
|
||||
R.string.action_start
|
||||
val isReading = remember(state.chapters) {
|
||||
state.chapters.fastAny { it.chapter.read }
|
||||
}
|
||||
Text(text = stringResource(id))
|
||||
Text(text = stringResource(if (isReading) R.string.action_resume else R.string.action_start))
|
||||
},
|
||||
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
||||
onClick = onContinueReading,
|
||||
|
@ -364,7 +379,7 @@ private fun MangaScreenSmallImpl(
|
|||
PullRefresh(
|
||||
refreshing = state.isRefreshingData,
|
||||
onRefresh = onRefresh,
|
||||
enabled = chapters.fastAll { !it.selected },
|
||||
enabled = !isAnySelected,
|
||||
indicatorPadding = WindowInsets.systemBars.only(WindowInsetsSides.Top).asPaddingValues(),
|
||||
) {
|
||||
val layoutDirection = LocalLayoutDirection.current
|
||||
|
@ -436,10 +451,13 @@ private fun MangaScreenSmallImpl(
|
|||
key = EntryScreenItem.ITEM_HEADER,
|
||||
contentType = EntryScreenItem.ITEM_HEADER,
|
||||
) {
|
||||
val missingItemsCount = remember(chapters) {
|
||||
chapters.map { it.chapter.chapterNumber }.missingItemsCount()
|
||||
}
|
||||
ItemHeader(
|
||||
enabled = chapters.fastAll { !it.selected },
|
||||
enabled = !isAnySelected,
|
||||
itemCount = chapters.size,
|
||||
missingItemsCount = chapters.map { it.chapter.chapterNumber }.missingItemsCount(),
|
||||
missingItemsCount = missingItemsCount,
|
||||
onClick = onFilterClicked,
|
||||
isManga = true,
|
||||
)
|
||||
|
@ -519,12 +537,18 @@ fun MangaScreenLargeImpl(
|
|||
|
||||
val chapters = remember(state) { state.processedChapters }
|
||||
|
||||
val isAnySelected by remember {
|
||||
derivedStateOf {
|
||||
chapters.fastAny { it.selected }
|
||||
}
|
||||
}
|
||||
|
||||
val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
|
||||
var topBarHeight by remember { mutableIntStateOf(0) }
|
||||
PullRefresh(
|
||||
refreshing = state.isRefreshingData,
|
||||
onRefresh = onRefresh,
|
||||
enabled = chapters.fastAll { !it.selected },
|
||||
enabled = !isAnySelected,
|
||||
indicatorPadding = PaddingValues(
|
||||
start = insetPadding.calculateStartPadding(layoutDirection),
|
||||
top = with(density) { topBarHeight.toDp() },
|
||||
|
@ -534,7 +558,7 @@ fun MangaScreenLargeImpl(
|
|||
val chapterListState = rememberLazyListState()
|
||||
|
||||
val internalOnBackPressed = {
|
||||
if (chapters.fastAny { it.selected }) {
|
||||
if (isAnySelected) {
|
||||
onAllChapterSelected(false)
|
||||
} else {
|
||||
onBackClicked()
|
||||
|
@ -544,10 +568,13 @@ fun MangaScreenLargeImpl(
|
|||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
val selectedChapterCount = remember(chapters) {
|
||||
chapters.count { it.selected }
|
||||
}
|
||||
EntryToolbar(
|
||||
modifier = Modifier.onSizeChanged { topBarHeight = it.height },
|
||||
title = state.manga.title,
|
||||
titleAlphaProvider = { if (chapters.fastAny { it.selected }) 1f else 0f },
|
||||
titleAlphaProvider = { if (isAnySelected) 1f else 0f },
|
||||
backgroundAlphaProvider = { 1f },
|
||||
hasFilters = state.manga.chaptersFiltered(),
|
||||
onBackClicked = internalOnBackPressed,
|
||||
|
@ -558,7 +585,7 @@ fun MangaScreenLargeImpl(
|
|||
onClickRefresh = onRefresh,
|
||||
onClickMigrate = onMigrateClicked,
|
||||
changeAnimeSkipIntro = null,
|
||||
actionModeCounter = chapters.count { it.selected },
|
||||
actionModeCounter = selectedChapterCount,
|
||||
onSelectAll = { onAllChapterSelected(true) },
|
||||
onInvertSelection = { onInvertSelection() },
|
||||
isManga = true,
|
||||
|
@ -570,8 +597,11 @@ fun MangaScreenLargeImpl(
|
|||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.BottomEnd,
|
||||
) {
|
||||
val selectedChapters = remember(chapters) {
|
||||
chapters.filter { it.selected }
|
||||
}
|
||||
SharedMangaBottomActionMenu(
|
||||
selected = chapters.filter { it.selected },
|
||||
selected = selectedChapters,
|
||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
||||
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
|
||||
|
@ -583,19 +613,20 @@ fun MangaScreenLargeImpl(
|
|||
},
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||
floatingActionButton = {
|
||||
val isFABVisible = remember(chapters) {
|
||||
chapters.fastAny { !it.chapter.read } && !isAnySelected
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = chapters.fastAny { !it.chapter.read } && chapters.fastAll { !it.selected },
|
||||
visible = isFABVisible,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
ExtendedFloatingActionButton(
|
||||
text = {
|
||||
val id = if (state.chapters.fastAny { it.chapter.read }) {
|
||||
R.string.action_resume
|
||||
} else {
|
||||
R.string.action_start
|
||||
val isReading = remember(state.chapters) {
|
||||
state.chapters.fastAny { it.chapter.read }
|
||||
}
|
||||
Text(text = stringResource(id))
|
||||
Text(text = stringResource(if (isReading) R.string.action_resume else R.string.action_start))
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
|
@ -671,10 +702,13 @@ fun MangaScreenLargeImpl(
|
|||
key = EntryScreenItem.ITEM_HEADER,
|
||||
contentType = EntryScreenItem.ITEM_HEADER,
|
||||
) {
|
||||
val missingItemsCount = remember(chapters) {
|
||||
chapters.map { it.chapter.chapterNumber }.missingItemsCount()
|
||||
}
|
||||
ItemHeader(
|
||||
enabled = chapters.fastAll { !it.selected },
|
||||
enabled = !isAnySelected,
|
||||
itemCount = chapters.size,
|
||||
missingItemsCount = chapters.map { it.chapter.chapterNumber }.missingItemsCount(),
|
||||
missingItemsCount = missingItemsCount,
|
||||
onClick = onFilterButtonClicked,
|
||||
isManga = true,
|
||||
)
|
||||
|
|
|
@ -298,7 +298,7 @@ fun ExpandableMangaDescription(
|
|||
) {
|
||||
tags.forEach {
|
||||
TagsChip(
|
||||
modifier = Modifier.padding(vertical = 4.dp),
|
||||
modifier = DefaultTagChipModifier,
|
||||
text = it,
|
||||
onClick = {
|
||||
tagSelected = it
|
||||
|
@ -314,7 +314,7 @@ fun ExpandableMangaDescription(
|
|||
) {
|
||||
items(items = tags) {
|
||||
TagsChip(
|
||||
modifier = Modifier.padding(vertical = 4.dp),
|
||||
modifier = DefaultTagChipModifier,
|
||||
text = it,
|
||||
onClick = {
|
||||
tagSelected = it
|
||||
|
@ -676,6 +676,8 @@ private fun MangaSummary(
|
|||
}
|
||||
}
|
||||
|
||||
private val DefaultTagChipModifier = Modifier.padding(vertical = 4.dp)
|
||||
|
||||
@Composable
|
||||
private fun TagsChip(
|
||||
text: String,
|
||||
|
|
|
@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.R
|
|||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
@ -329,12 +328,6 @@ object SettingsReaderScreen : SearchableSettings {
|
|||
subtitle = stringResource(R.string.pref_dual_page_invert_summary),
|
||||
enabled = dualPageSplit,
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.longStripSplitWebtoon(),
|
||||
title = stringResource(R.string.pref_long_strip_split),
|
||||
subtitle = stringResource(R.string.split_tall_images_summary),
|
||||
enabled = !isReleaseBuildType, // TODO: Show in release build when the feature is stable
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.webtoonDoubleTapZoomEnabled(),
|
||||
title = stringResource(R.string.pref_double_tap_zoom),
|
||||
|
@ -373,11 +366,6 @@ object SettingsReaderScreen : SearchableSettings {
|
|||
pref = readerPreferences.readWithLongTap(),
|
||||
title = stringResource(R.string.pref_read_with_long_tap),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.folderPerManga(),
|
||||
title = stringResource(R.string.pref_create_folder_per_manga),
|
||||
subtitle = stringResource(R.string.pref_create_folder_per_manga_summary),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -272,7 +272,7 @@ private fun getIndex() = settingScreens
|
|||
SettingsData(
|
||||
title = stringResource(screen.getTitleRes()),
|
||||
route = screen,
|
||||
contents = screen.sourcePreferences(),
|
||||
contents = screen.getPreferences(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -23,10 +23,6 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.unit.dp
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import compose.icons.SimpleIcons
|
||||
import compose.icons.simpleicons.Discord
|
||||
import compose.icons.simpleicons.Github
|
||||
import compose.icons.simpleicons.Reddit
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.more.LogoHeader
|
||||
|
@ -51,6 +47,10 @@ import tachiyomi.domain.release.interactor.GetApplicationRelease
|
|||
import tachiyomi.presentation.core.components.LinkIcon
|
||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.icons.CustomIcons
|
||||
import tachiyomi.presentation.core.icons.Discord
|
||||
import tachiyomi.presentation.core.icons.Github
|
||||
import tachiyomi.presentation.core.icons.Reddit
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.text.DateFormat
|
||||
|
@ -183,17 +183,17 @@ object AboutScreen : Screen() {
|
|||
)
|
||||
LinkIcon(
|
||||
label = "Discord",
|
||||
icon = SimpleIcons.Discord,
|
||||
icon = CustomIcons.Discord,
|
||||
url = "https://discord.gg/F32UjdJZrR",
|
||||
)
|
||||
LinkIcon(
|
||||
label = "Reddit",
|
||||
icon = SimpleIcons.Reddit,
|
||||
icon = CustomIcons.Reddit,
|
||||
url = "https://www.reddit.com/r/Aniyomi",
|
||||
)
|
||||
LinkIcon(
|
||||
label = "GitHub",
|
||||
icon = SimpleIcons.Github,
|
||||
icon = CustomIcons.Github,
|
||||
url = "https://github.com/aniyomiorg/aniyomi",
|
||||
)
|
||||
}
|
||||
|
|
|
@ -6,12 +6,10 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.text.ExperimentalTextApi
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@OptIn(ExperimentalTextApi::class)
|
||||
@Composable
|
||||
fun PageIndicatorText(
|
||||
currentPage: Int,
|
||||
|
|
|
@ -16,7 +16,6 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
|||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
|
||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
||||
import tachiyomi.presentation.core.components.CheckboxItem
|
||||
import tachiyomi.presentation.core.components.HeadingItem
|
||||
import tachiyomi.presentation.core.components.SettingsChipRow
|
||||
|
@ -193,13 +192,6 @@ private fun ColumnScope.WebtoonViewerSettings(screenModel: ReaderSettingsScreenM
|
|||
)
|
||||
}
|
||||
|
||||
if (!isReleaseBuildType) {
|
||||
CheckboxItem(
|
||||
label = stringResource(R.string.pref_long_strip_split),
|
||||
pref = screenModel.preferences.longStripSplitWebtoon(),
|
||||
)
|
||||
}
|
||||
|
||||
CheckboxItem(
|
||||
label = stringResource(R.string.pref_double_tap_zoom),
|
||||
pref = screenModel.preferences.webtoonDoubleTapZoomEnabled(),
|
||||
|
|
|
@ -17,7 +17,6 @@ import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences
|
|||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.util.system.workManager
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
|
@ -492,13 +491,6 @@ object Migrations {
|
|||
if (oldVersion < 100) {
|
||||
BackupCreateJob.setupTask(context)
|
||||
}
|
||||
if (oldVersion < 102) {
|
||||
// This was accidentally visible from the reader settings sheet, but should always
|
||||
// be disabled in release builds.
|
||||
if (isReleaseBuildType) {
|
||||
readerPreferences.longStripSplitWebtoon().set(false)
|
||||
}
|
||||
}
|
||||
if (oldVersion < 105) {
|
||||
val pref = libraryPreferences.autoUpdateDeviceRestrictions()
|
||||
if (pref.isSet() && "battery_not_low" in pref.get()) {
|
||||
|
|
|
@ -4,17 +4,24 @@ package eu.kanade.tachiyomi.data.backup
|
|||
internal object BackupConst {
|
||||
const val BACKUP_CATEGORY = 0x1
|
||||
const val BACKUP_CATEGORY_MASK = 0x1
|
||||
|
||||
const val BACKUP_CHAPTER = 0x2
|
||||
const val BACKUP_CHAPTER_MASK = 0x2
|
||||
|
||||
const val BACKUP_HISTORY = 0x4
|
||||
const val BACKUP_HISTORY_MASK = 0x4
|
||||
|
||||
const val BACKUP_TRACK = 0x8
|
||||
const val BACKUP_TRACK_MASK = 0x8
|
||||
|
||||
const val BACKUP_PREFS = 0x10
|
||||
const val BACKUP_PREFS_MASK = 0x10
|
||||
|
||||
const val BACKUP_EXT_PREFS = 0x20
|
||||
const val BACKUP_EXT_PREFS_MASK = 0x20
|
||||
|
||||
const val BACKUP_EXTENSIONS = 0x40
|
||||
const val BACKUP_EXTENSIONS_MASK = 0x40
|
||||
|
||||
const val BACKUP_ALL = 0x7F
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
|
|||
}
|
||||
|
||||
return try {
|
||||
val location = BackupManager(context).createBackup(uri, flags, isAutoBackup)
|
||||
val location = BackupCreator(context).createBackup(uri, flags, isAutoBackup)
|
||||
if (!isAutoBackup) {
|
||||
notifier.showBackupComplete(
|
||||
UniFile.fromUri(context, location.toUri()),
|
||||
|
|
|
@ -0,0 +1,449 @@
|
|||
package eu.kanade.tachiyomi.data.backup
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupAnime
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupAnimeHistory
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupAnimeSource
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupExtension
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupExtensionPreferences
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupSource
|
||||
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.backupAnimeTrackMapper
|
||||
import eu.kanade.tachiyomi.data.backup.models.backupCategoryMapper
|
||||
import eu.kanade.tachiyomi.data.backup.models.backupChapterMapper
|
||||
import eu.kanade.tachiyomi.data.backup.models.backupEpisodeMapper
|
||||
import eu.kanade.tachiyomi.data.backup.models.backupTrackMapper
|
||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||
import eu.kanade.tachiyomi.source.anime.getPreferenceKey
|
||||
import eu.kanade.tachiyomi.source.manga.getPreferenceKey
|
||||
import eu.kanade.tachiyomi.util.system.hasPermission
|
||||
import kotlinx.serialization.protobuf.ProtoBuf
|
||||
import logcat.LogPriority
|
||||
import okio.buffer
|
||||
import okio.gzip
|
||||
import okio.sink
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.data.handlers.anime.AnimeDatabaseHandler
|
||||
import tachiyomi.data.handlers.manga.MangaDatabaseHandler
|
||||
import tachiyomi.domain.backup.service.BackupPreferences
|
||||
import tachiyomi.domain.category.anime.interactor.GetAnimeCategories
|
||||
import tachiyomi.domain.category.manga.interactor.GetMangaCategories
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.domain.entries.anime.interactor.GetAnimeFavorites
|
||||
import tachiyomi.domain.entries.anime.model.Anime
|
||||
import tachiyomi.domain.entries.manga.interactor.GetMangaFavorites
|
||||
import tachiyomi.domain.entries.manga.model.Manga
|
||||
import tachiyomi.domain.history.anime.interactor.GetAnimeHistory
|
||||
import tachiyomi.domain.history.manga.interactor.GetMangaHistory
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.domain.source.anime.service.AnimeSourceManager
|
||||
import tachiyomi.domain.source.manga.service.MangaSourceManager
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
class BackupCreator(
|
||||
private val context: Context,
|
||||
) {
|
||||
|
||||
private val mangaHandler: MangaDatabaseHandler = Injekt.get()
|
||||
private val animeHandler: AnimeDatabaseHandler = Injekt.get()
|
||||
private val mangaSourceManager: MangaSourceManager = Injekt.get()
|
||||
private val animeSourceManager: AnimeSourceManager = Injekt.get()
|
||||
private val backupPreferences: BackupPreferences = Injekt.get()
|
||||
private val libraryPreferences: LibraryPreferences = Injekt.get()
|
||||
private val getMangaCategories: GetMangaCategories = Injekt.get()
|
||||
private val getAnimeCategories: GetAnimeCategories = Injekt.get()
|
||||
private val getMangaFavorites: GetMangaFavorites = Injekt.get()
|
||||
private val getAnimeFavorites: GetAnimeFavorites = Injekt.get()
|
||||
private val getMangaHistory: GetMangaHistory = Injekt.get()
|
||||
private val getAnimeHistory: GetAnimeHistory = Injekt.get()
|
||||
|
||||
internal val parser = ProtoBuf
|
||||
|
||||
/**
|
||||
* Create backup file from database
|
||||
*
|
||||
* @param uri path of Uri
|
||||
* @param isAutoBackup backup called from scheduled backup job
|
||||
*/
|
||||
suspend fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String {
|
||||
if (!context.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||
throw IllegalStateException(context.getString(R.string.missing_storage_permission))
|
||||
}
|
||||
|
||||
val databaseAnime = getAnimeFavorites.await()
|
||||
val databaseManga = getMangaFavorites.await()
|
||||
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
val backup = Backup(
|
||||
backupMangas(databaseManga, flags),
|
||||
backupCategories(flags),
|
||||
backupAnimes(databaseAnime, flags),
|
||||
backupAnimeCategories(flags),
|
||||
emptyList(),
|
||||
prepExtensionInfoForSync(databaseManga),
|
||||
emptyList(),
|
||||
prepAnimeExtensionInfoForSync(databaseAnime),
|
||||
backupPreferences(prefs, flags),
|
||||
backupExtensionPreferences(flags),
|
||||
backupExtensions(flags),
|
||||
)
|
||||
|
||||
var file: UniFile? = null
|
||||
try {
|
||||
file = (
|
||||
if (isAutoBackup) {
|
||||
// Get dir of file and create
|
||||
var dir = UniFile.fromUri(context, uri)
|
||||
dir = dir.createDirectory("automatic")
|
||||
|
||||
// Delete older backups
|
||||
val numberOfBackups = backupPreferences.numberOfBackups().get()
|
||||
dir.listFiles { _, filename -> Backup.filenameRegex.matches(filename) }
|
||||
.orEmpty()
|
||||
.sortedByDescending { it.name }
|
||||
.drop(numberOfBackups - 1)
|
||||
.forEach { it.delete() }
|
||||
|
||||
// Create new file to place backup
|
||||
dir.createFile(Backup.getFilename())
|
||||
} else {
|
||||
UniFile.fromUri(context, uri)
|
||||
}
|
||||
)
|
||||
?: throw Exception(context.getString(R.string.create_backup_file_error))
|
||||
|
||||
if (!file.isFile) {
|
||||
throw IllegalStateException("Failed to get handle on a backup file")
|
||||
}
|
||||
|
||||
val byteArray = parser.encodeToByteArray(BackupSerializer, backup)
|
||||
if (byteArray.isEmpty()) {
|
||||
throw IllegalStateException(context.getString(R.string.empty_backup_error))
|
||||
}
|
||||
|
||||
file.openOutputStream().also {
|
||||
// Force overwrite old file
|
||||
(it as? FileOutputStream)?.channel?.truncate(0)
|
||||
}.sink().gzip().buffer().use { it.write(byteArray) }
|
||||
val fileUri = file.uri
|
||||
|
||||
// Make sure it's a valid backup file
|
||||
BackupFileValidator().validate(context, fileUri)
|
||||
|
||||
return fileUri.toString()
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
file?.delete()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private fun prepExtensionInfoForSync(mangas: List<Manga>): List<BackupSource> {
|
||||
return mangas
|
||||
.asSequence()
|
||||
.map(Manga::source)
|
||||
.distinct()
|
||||
.map(mangaSourceManager::getOrStub)
|
||||
.map(BackupSource::copyFrom)
|
||||
.toList()
|
||||
}
|
||||
|
||||
private fun prepAnimeExtensionInfoForSync(animes: List<Anime>): List<BackupAnimeSource> {
|
||||
return animes
|
||||
.asSequence()
|
||||
.map(Anime::source)
|
||||
.distinct()
|
||||
.map(animeSourceManager::getOrStub)
|
||||
.map(BackupAnimeSource::copyFrom)
|
||||
.toList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup the categories of manga library
|
||||
*
|
||||
* @return list of [BackupCategory] to be backed up
|
||||
*/
|
||||
private suspend fun backupCategories(options: Int): List<BackupCategory> {
|
||||
// Check if user wants category information in backup
|
||||
return if (options and BackupConst.BACKUP_CATEGORY_MASK == BackupConst.BACKUP_CATEGORY) {
|
||||
getMangaCategories.await()
|
||||
.filterNot(Category::isSystemCategory)
|
||||
.map(backupCategoryMapper)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup the categories of anime library
|
||||
*
|
||||
* @return list of [BackupCategory] to be backed up
|
||||
*/
|
||||
private suspend fun backupAnimeCategories(options: Int): List<BackupCategory> {
|
||||
// Check if user wants category information in backup
|
||||
return if (options and BackupConst.BACKUP_CATEGORY_MASK == BackupConst.BACKUP_CATEGORY) {
|
||||
getAnimeCategories.await()
|
||||
.filterNot(Category::isSystemCategory)
|
||||
.map(backupCategoryMapper)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun backupMangas(mangas: List<Manga>, flags: Int): List<BackupManga> {
|
||||
return mangas.map {
|
||||
backupManga(it, flags)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun backupAnimes(animes: List<Anime>, flags: Int): List<BackupAnime> {
|
||||
return animes.map {
|
||||
backupAnime(it, flags)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a manga to Json
|
||||
*
|
||||
* @param manga manga that gets converted
|
||||
* @param options options for the backup
|
||||
* @return [BackupManga] containing manga in a serializable form
|
||||
*/
|
||||
private 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 BackupConst.BACKUP_CHAPTER_MASK == BackupConst.BACKUP_CHAPTER) {
|
||||
// Backup all the chapters
|
||||
val chapters = mangaHandler.awaitList {
|
||||
chaptersQueries.getChaptersByMangaId(
|
||||
manga.id,
|
||||
backupChapterMapper,
|
||||
)
|
||||
}
|
||||
if (chapters.isNotEmpty()) {
|
||||
mangaObject.chapters = chapters
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user wants category information in backup
|
||||
if (options and BackupConst.BACKUP_CATEGORY_MASK == BackupConst.BACKUP_CATEGORY) {
|
||||
// Backup categories for this manga
|
||||
val categoriesForManga = getMangaCategories.await(manga.id)
|
||||
if (categoriesForManga.isNotEmpty()) {
|
||||
mangaObject.categories = categoriesForManga.map { it.order }
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user wants track information in backup
|
||||
if (options and BackupConst.BACKUP_TRACK_MASK == BackupConst.BACKUP_TRACK) {
|
||||
val tracks = mangaHandler.awaitList {
|
||||
manga_syncQueries.getTracksByMangaId(
|
||||
manga.id,
|
||||
backupTrackMapper,
|
||||
)
|
||||
}
|
||||
if (tracks.isNotEmpty()) {
|
||||
mangaObject.tracking = tracks
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user wants history information in backup
|
||||
if (options and BackupConst.BACKUP_HISTORY_MASK == BackupConst.BACKUP_HISTORY) {
|
||||
val historyByMangaId = getMangaHistory.await(manga.id)
|
||||
if (historyByMangaId.isNotEmpty()) {
|
||||
val history = historyByMangaId.map { history ->
|
||||
val chapter = mangaHandler.awaitOne {
|
||||
chaptersQueries.getChapterById(
|
||||
history.chapterId,
|
||||
)
|
||||
}
|
||||
BackupHistory(chapter.url, history.readAt?.time ?: 0L, history.readDuration)
|
||||
}
|
||||
if (history.isNotEmpty()) {
|
||||
mangaObject.history = history
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mangaObject
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an anime to Json
|
||||
*
|
||||
* @param anime anime that gets converted
|
||||
* @param options options for the backup
|
||||
* @return [BackupAnime] containing anime in a serializable form
|
||||
*/
|
||||
private suspend fun backupAnime(anime: Anime, options: Int): BackupAnime {
|
||||
// Entry for this anime
|
||||
val animeObject = BackupAnime.copyFrom(anime)
|
||||
|
||||
// Check if user wants chapter information in backup
|
||||
if (options and BackupConst.BACKUP_CHAPTER_MASK == BackupConst.BACKUP_CHAPTER) {
|
||||
// Backup all the chapters
|
||||
val episodes = animeHandler.awaitList {
|
||||
episodesQueries.getEpisodesByAnimeId(
|
||||
anime.id,
|
||||
backupEpisodeMapper,
|
||||
)
|
||||
}
|
||||
if (episodes.isNotEmpty()) {
|
||||
animeObject.episodes = episodes
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user wants category information in backup
|
||||
if (options and BackupConst.BACKUP_CATEGORY_MASK == BackupConst.BACKUP_CATEGORY) {
|
||||
// Backup categories for this manga
|
||||
val categoriesForAnime = getAnimeCategories.await(anime.id)
|
||||
if (categoriesForAnime.isNotEmpty()) {
|
||||
animeObject.categories = categoriesForAnime.map { it.order }
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user wants track information in backup
|
||||
if (options and BackupConst.BACKUP_TRACK_MASK == BackupConst.BACKUP_TRACK) {
|
||||
val tracks = animeHandler.awaitList {
|
||||
anime_syncQueries.getTracksByAnimeId(
|
||||
anime.id,
|
||||
backupAnimeTrackMapper,
|
||||
)
|
||||
}
|
||||
if (tracks.isNotEmpty()) {
|
||||
animeObject.tracking = tracks
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user wants history information in backup
|
||||
if (options and BackupConst.BACKUP_HISTORY_MASK == BackupConst.BACKUP_HISTORY) {
|
||||
val historyByAnimeId = getAnimeHistory.await(anime.id)
|
||||
if (historyByAnimeId.isNotEmpty()) {
|
||||
val history = historyByAnimeId.map { history ->
|
||||
val episode = animeHandler.awaitOne {
|
||||
episodesQueries.getEpisodeById(
|
||||
history.episodeId,
|
||||
)
|
||||
}
|
||||
BackupAnimeHistory(episode.url, history.seenAt?.time ?: 0L)
|
||||
}
|
||||
if (history.isNotEmpty()) {
|
||||
animeObject.history = history
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return animeObject
|
||||
}
|
||||
|
||||
private fun backupExtensionPreferences(flags: Int): List<BackupExtensionPreferences> {
|
||||
if (flags and BackupConst.BACKUP_EXT_PREFS_MASK != BackupConst.BACKUP_EXT_PREFS) return emptyList()
|
||||
val prefs = mutableListOf<Pair<String, SharedPreferences>>()
|
||||
Injekt.get<AnimeSourceManager>().getOnlineSources().forEach {
|
||||
val name = it.getPreferenceKey()
|
||||
prefs += Pair(name, context.getSharedPreferences(name, 0x0))
|
||||
}
|
||||
Injekt.get<MangaSourceManager>().getOnlineSources().forEach {
|
||||
val name = it.getPreferenceKey()
|
||||
prefs += Pair(name, context.getSharedPreferences(name, 0x0))
|
||||
}
|
||||
return prefs.map {
|
||||
BackupExtensionPreferences(
|
||||
it.first,
|
||||
backupPreferences(it.second, BackupConst.BACKUP_PREFS),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun backupExtensions(flags: Int): List<BackupExtension> {
|
||||
if (flags and BackupConst.BACKUP_EXTENSIONS_MASK != BackupConst.BACKUP_EXTENSIONS) return emptyList()
|
||||
val installedExtensions = mutableListOf<BackupExtension>()
|
||||
Injekt.get<AnimeExtensionManager>().installedExtensionsFlow.value.forEach {
|
||||
val packageName = it.pkgName
|
||||
val apk = File(
|
||||
context.packageManager
|
||||
.getApplicationInfo(
|
||||
packageName,
|
||||
PackageManager.GET_META_DATA,
|
||||
).publicSourceDir,
|
||||
).readBytes()
|
||||
installedExtensions.add(
|
||||
BackupExtension(packageName, apk),
|
||||
)
|
||||
}
|
||||
Injekt.get<MangaExtensionManager>().installedExtensionsFlow.value.forEach {
|
||||
val packageName = it.pkgName
|
||||
val apk = File(
|
||||
context.packageManager
|
||||
.getApplicationInfo(
|
||||
packageName,
|
||||
PackageManager.GET_META_DATA,
|
||||
).publicSourceDir,
|
||||
).readBytes()
|
||||
installedExtensions.add(
|
||||
BackupExtension(packageName, apk),
|
||||
)
|
||||
}
|
||||
return installedExtensions
|
||||
}
|
||||
|
||||
private fun backupPreferences(prefs: SharedPreferences, flags: Int): List<BackupPreference> {
|
||||
if (flags and BackupConst.BACKUP_PREFS_MASK != BackupConst.BACKUP_PREFS) return emptyList()
|
||||
val backupPreferences = mutableListOf<BackupPreference>()
|
||||
for (pref in prefs.all) {
|
||||
val toAdd = when (pref.value) {
|
||||
is Int -> {
|
||||
BackupPreference(pref.key, IntPreferenceValue(pref.value as Int))
|
||||
}
|
||||
is Long -> {
|
||||
BackupPreference(pref.key, LongPreferenceValue(pref.value as Long))
|
||||
}
|
||||
is Float -> {
|
||||
BackupPreference(pref.key, FloatPreferenceValue(pref.value as Float))
|
||||
}
|
||||
is String -> {
|
||||
BackupPreference(pref.key, StringPreferenceValue(pref.value as String))
|
||||
}
|
||||
is Boolean -> {
|
||||
BackupPreference(pref.key, BooleanPreferenceValue(pref.value as Boolean))
|
||||
}
|
||||
is Set<*> -> {
|
||||
(pref.value as? Set<String>)?.let {
|
||||
BackupPreference(pref.key, StringSetPreferenceValue(it))
|
||||
} ?: continue
|
||||
}
|
||||
else -> {
|
||||
continue
|
||||
}
|
||||
}
|
||||
backupPreferences.add(toAdd)
|
||||
}
|
||||
return backupPreferences
|
||||
}
|
||||
}
|
|
@ -78,5 +78,8 @@ class BackupFileValidator(
|
|||
return Results(missingSources, missingTrackers)
|
||||
}
|
||||
|
||||
data class Results(val missingSources: List<String>, val missingTrackers: List<String>)
|
||||
data class Results(
|
||||
val missingSources: List<String>,
|
||||
val missingTrackers: List<String>,
|
||||
)
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -26,7 +26,7 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet
|
|||
override suspend fun doWork(): Result {
|
||||
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
|
||||
?: return Result.failure()
|
||||
val sync = inputData.getBoolean(SYNC, false)
|
||||
val sync = inputData.getBoolean(SYNC_KEY, false)
|
||||
|
||||
try {
|
||||
setForeground(getForegroundInfo())
|
||||
|
@ -67,7 +67,7 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet
|
|||
fun start(context: Context, uri: Uri, sync: Boolean = false) {
|
||||
val inputData = workDataOf(
|
||||
LOCATION_URI_KEY to uri.toString(),
|
||||
SYNC to sync,
|
||||
SYNC_KEY to sync,
|
||||
)
|
||||
val request = OneTimeWorkRequestBuilder<BackupRestoreJob>()
|
||||
.addTag(TAG)
|
||||
|
@ -86,4 +86,4 @@ private const val TAG = "BackupRestore"
|
|||
|
||||
private const val LOCATION_URI_KEY = "location_uri" // String
|
||||
|
||||
private const val SYNC = "sync" // Boolean
|
||||
private const val SYNC_KEY = "sync" // Boolean
|
||||
|
|
|
@ -5,8 +5,14 @@ import android.content.Intent
|
|||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import androidx.preference.PreferenceManager
|
||||
import data.Manga_sync
|
||||
import data.Mangas
|
||||
import dataanime.Anime_sync
|
||||
import dataanime.Animes
|
||||
import eu.kanade.domain.entries.anime.interactor.UpdateAnime
|
||||
import eu.kanade.domain.entries.manga.interactor.UpdateManga
|
||||
import eu.kanade.domain.items.chapter.model.copyFrom
|
||||
import eu.kanade.domain.items.episode.model.copyFrom
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupAnime
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupAnimeHistory
|
||||
|
@ -24,20 +30,28 @@ 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.anime.model.copyFrom
|
||||
import eu.kanade.tachiyomi.source.manga.model.copyFrom
|
||||
import eu.kanade.tachiyomi.util.BackupUtil
|
||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.isActive
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.data.UpdateStrategyColumnAdapter
|
||||
import tachiyomi.data.handlers.anime.AnimeDatabaseHandler
|
||||
import tachiyomi.data.handlers.manga.MangaDatabaseHandler
|
||||
import tachiyomi.domain.category.anime.interactor.GetAnimeCategories
|
||||
import tachiyomi.domain.category.manga.interactor.GetMangaCategories
|
||||
import tachiyomi.domain.entries.anime.interactor.AnimeFetchInterval
|
||||
import tachiyomi.domain.entries.anime.model.Anime
|
||||
import tachiyomi.domain.entries.manga.interactor.MangaFetchInterval
|
||||
import tachiyomi.domain.entries.manga.model.Manga
|
||||
import tachiyomi.domain.history.anime.model.AnimeHistoryUpdate
|
||||
import tachiyomi.domain.history.manga.model.MangaHistoryUpdate
|
||||
import tachiyomi.domain.items.chapter.model.Chapter
|
||||
import tachiyomi.domain.items.chapter.repository.ChapterRepository
|
||||
import tachiyomi.domain.items.episode.model.Episode
|
||||
import tachiyomi.domain.items.episode.repository.EpisodeRepository
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.domain.track.anime.model.AnimeTrack
|
||||
import tachiyomi.domain.track.manga.model.MangaTrack
|
||||
import uy.kohesive.injekt.Injekt
|
||||
|
@ -47,25 +61,29 @@ import java.text.SimpleDateFormat
|
|||
import java.time.ZonedDateTime
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import kotlin.math.max
|
||||
|
||||
class BackupRestorer(
|
||||
private val context: Context,
|
||||
private val notifier: BackupNotifier,
|
||||
) {
|
||||
|
||||
private val mangaHandler: MangaDatabaseHandler = Injekt.get()
|
||||
private val updateManga: UpdateManga = Injekt.get()
|
||||
private val chapterRepository: ChapterRepository = Injekt.get()
|
||||
private val getMangaCategories: GetMangaCategories = Injekt.get()
|
||||
private val mangaFetchInterval: MangaFetchInterval = Injekt.get()
|
||||
|
||||
private val animeHandler: AnimeDatabaseHandler = Injekt.get()
|
||||
private val updateAnime: UpdateAnime = Injekt.get()
|
||||
private val episodeRepository: EpisodeRepository = Injekt.get()
|
||||
private val getAnimeCategories: GetAnimeCategories = Injekt.get()
|
||||
private val animeFetchInterval: AnimeFetchInterval = Injekt.get()
|
||||
|
||||
private val libraryPreferences: LibraryPreferences = Injekt.get()
|
||||
|
||||
private var now = ZonedDateTime.now()
|
||||
private var currentMangaFetchWindow = mangaFetchInterval.getWindow(now)
|
||||
private var currentAnimeFetchWindow = animeFetchInterval.getWindow(now)
|
||||
|
||||
private var backupManager = BackupManager(context)
|
||||
|
||||
private var restoreAmount = 0
|
||||
private var restoreProgress = 0
|
||||
|
||||
|
@ -128,7 +146,7 @@ class BackupRestorer(
|
|||
private suspend fun performRestore(uri: Uri, sync: Boolean): Boolean {
|
||||
val backup = BackupUtil.decodeBackup(context, uri)
|
||||
|
||||
restoreAmount = backup.backupManga.size + backup.backupAnime.size + 2 // +2 for categories
|
||||
restoreAmount = backup.backupManga.size + backup.backupAnime.size + 3 // +3 for categories, app prefs, source prefs
|
||||
|
||||
// Restore categories
|
||||
if (backup.backupCategories.isNotEmpty()) {
|
||||
|
@ -194,7 +212,38 @@ class BackupRestorer(
|
|||
}
|
||||
|
||||
private suspend fun restoreCategories(backupCategories: List<BackupCategory>) {
|
||||
backupManager.restoreCategories(backupCategories)
|
||||
// Get categories from file and from db
|
||||
val dbCategories = getMangaCategories.await()
|
||||
|
||||
val categories = backupCategories.map {
|
||||
var category = it.getCategory()
|
||||
var found = false
|
||||
for (dbCategory in dbCategories) {
|
||||
// If the category is already in the db, assign the id to the file's category
|
||||
// and do nothing
|
||||
if (category.name == dbCategory.name) {
|
||||
category = category.copy(id = dbCategory.id)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
// Let the db assign the id
|
||||
val id = mangaHandler.awaitOneExecutable {
|
||||
categoriesQueries.insert(category.name, category.order, category.flags)
|
||||
categoriesQueries.selectLastInsertedRowId()
|
||||
}
|
||||
category = category.copy(id = id)
|
||||
}
|
||||
|
||||
category
|
||||
}
|
||||
|
||||
libraryPreferences.categorizedDisplaySettings().set(
|
||||
(dbCategories + categories)
|
||||
.distinctBy { it.flags }
|
||||
.size > 1,
|
||||
)
|
||||
|
||||
restoreProgress += 1
|
||||
showRestoreProgress(
|
||||
|
@ -206,7 +255,38 @@ class BackupRestorer(
|
|||
}
|
||||
|
||||
private suspend fun restoreAnimeCategories(backupCategories: List<BackupCategory>) {
|
||||
backupManager.restoreAnimeCategories(backupCategories)
|
||||
// Get categories from file and from db
|
||||
val dbCategories = getAnimeCategories.await()
|
||||
|
||||
val categories = backupCategories.map {
|
||||
var category = it.getCategory()
|
||||
var found = false
|
||||
for (dbCategory in dbCategories) {
|
||||
// If the category is already in the db, assign the id to the file's category
|
||||
// and do nothing
|
||||
if (category.name == dbCategory.name) {
|
||||
category = category.copy(id = dbCategory.id)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
// Let the db assign the id
|
||||
val id = animeHandler.awaitOneExecutable {
|
||||
categoriesQueries.insert(category.name, category.order, category.flags)
|
||||
categoriesQueries.selectLastInsertedRowId()
|
||||
}
|
||||
category = category.copy(id = id)
|
||||
}
|
||||
|
||||
category
|
||||
}
|
||||
|
||||
libraryPreferences.categorizedDisplaySettings().set(
|
||||
(dbCategories + categories)
|
||||
.distinctBy { it.flags }
|
||||
.size > 1,
|
||||
)
|
||||
|
||||
restoreProgress += 1
|
||||
showRestoreProgress(
|
||||
|
@ -230,14 +310,14 @@ class BackupRestorer(
|
|||
val tracks = backupManga.getTrackingImpl()
|
||||
|
||||
try {
|
||||
val dbManga = backupManager.getMangaFromDatabase(manga.url, manga.source)
|
||||
val dbManga = getMangaFromDatabase(manga.url, manga.source)
|
||||
val restoredManga = if (dbManga == null) {
|
||||
// Manga not in database
|
||||
restoreExistingManga(manga, chapters, categories, history, tracks, backupCategories)
|
||||
} else {
|
||||
// Manga in database
|
||||
// Copy information from manga already in database
|
||||
val updateManga = backupManager.restoreExistingManga(manga, dbManga)
|
||||
val updateManga = restoreExistingManga(manga, dbManga)
|
||||
// Fetch rest of manga information
|
||||
restoreNewManga(
|
||||
updateManga,
|
||||
|
@ -272,6 +352,50 @@ class BackupRestorer(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns manga
|
||||
*
|
||||
* @return [Manga], null if not found
|
||||
*/
|
||||
private suspend fun getMangaFromDatabase(url: String, source: Long): Mangas? {
|
||||
return mangaHandler.awaitOneOrNull { mangasQueries.getMangaByUrlAndSource(url, source) }
|
||||
}
|
||||
|
||||
private suspend fun restoreExistingManga(manga: Manga, dbManga: Mangas): Manga {
|
||||
var updatedManga = manga.copy(id = dbManga._id)
|
||||
updatedManga = updatedManga.copyFrom(dbManga)
|
||||
updateManga(updatedManga)
|
||||
return updatedManga
|
||||
}
|
||||
|
||||
private suspend fun updateManga(manga: Manga): Long {
|
||||
mangaHandler.await(true) {
|
||||
mangasQueries.update(
|
||||
source = manga.source,
|
||||
url = manga.url,
|
||||
artist = manga.artist,
|
||||
author = manga.author,
|
||||
description = manga.description,
|
||||
genre = manga.genre?.joinToString(separator = ", "),
|
||||
title = manga.title,
|
||||
status = manga.status,
|
||||
thumbnailUrl = manga.thumbnailUrl,
|
||||
favorite = manga.favorite,
|
||||
lastUpdate = manga.lastUpdate,
|
||||
nextUpdate = null,
|
||||
calculateInterval = null,
|
||||
initialized = manga.initialized,
|
||||
viewer = manga.viewerFlags,
|
||||
chapterFlags = manga.chapterFlags,
|
||||
coverLastModified = manga.coverLastModified,
|
||||
dateAdded = manga.dateAdded,
|
||||
mangaId = manga.id!!,
|
||||
updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode),
|
||||
)
|
||||
}
|
||||
return manga.id
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches manga information
|
||||
*
|
||||
|
@ -287,12 +411,139 @@ class BackupRestorer(
|
|||
tracks: List<MangaTrack>,
|
||||
backupCategories: List<BackupCategory>,
|
||||
): Manga {
|
||||
val fetchedManga = backupManager.restoreNewManga(manga)
|
||||
backupManager.restoreChapters(fetchedManga, chapters)
|
||||
val fetchedManga = restoreNewManga(manga)
|
||||
restoreChapters(fetchedManga, chapters)
|
||||
restoreExtras(fetchedManga, categories, history, tracks, backupCategories)
|
||||
return fetchedManga
|
||||
}
|
||||
|
||||
private suspend fun restoreChapters(
|
||||
manga: Manga,
|
||||
chapters: List<Chapter>,
|
||||
) {
|
||||
val dbChapters = mangaHandler.awaitList { chaptersQueries.getChaptersByMangaId(manga.id!!) }
|
||||
|
||||
val processed = chapters.map { chapter ->
|
||||
var updatedChapter = chapter
|
||||
val dbChapter = dbChapters.find { it.url == updatedChapter.url }
|
||||
if (dbChapter != null) {
|
||||
updatedChapter = updatedChapter.copy(id = dbChapter._id)
|
||||
updatedChapter = updatedChapter.copyFrom(dbChapter)
|
||||
if (dbChapter.read && !updatedChapter.read) {
|
||||
updatedChapter = updatedChapter.copy(
|
||||
read = true,
|
||||
lastPageRead = dbChapter.last_page_read,
|
||||
)
|
||||
} else if (chapter.lastPageRead == 0L && dbChapter.last_page_read != 0L) {
|
||||
updatedChapter = updatedChapter.copy(lastPageRead = dbChapter.last_page_read)
|
||||
}
|
||||
if (!updatedChapter.bookmark && dbChapter.bookmark) {
|
||||
updatedChapter = updatedChapter.copy(bookmark = true)
|
||||
}
|
||||
}
|
||||
|
||||
updatedChapter.copy(mangaId = manga.id ?: -1)
|
||||
}
|
||||
|
||||
val newChapters = processed.groupBy { it.id > 0 }
|
||||
newChapters[true]?.let { updateKnownChapters(it) }
|
||||
newChapters[false]?.let { insertChapters(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts list of chapters
|
||||
*/
|
||||
private suspend fun insertChapters(chapters: List<tachiyomi.domain.items.chapter.model.Chapter>) {
|
||||
mangaHandler.await(true) {
|
||||
chapters.forEach { chapter ->
|
||||
chaptersQueries.insert(
|
||||
chapter.mangaId,
|
||||
chapter.url,
|
||||
chapter.name,
|
||||
chapter.scanlator,
|
||||
chapter.read,
|
||||
chapter.bookmark,
|
||||
chapter.lastPageRead,
|
||||
chapter.chapterNumber,
|
||||
chapter.sourceOrder,
|
||||
chapter.dateFetch,
|
||||
chapter.dateUpload,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a list of chapters with known database ids
|
||||
*/
|
||||
private suspend fun updateKnownChapters(
|
||||
chapters: List<tachiyomi.domain.items.chapter.model.Chapter>,
|
||||
) {
|
||||
mangaHandler.await(true) {
|
||||
chapters.forEach { chapter ->
|
||||
chaptersQueries.update(
|
||||
mangaId = null,
|
||||
url = null,
|
||||
name = null,
|
||||
scanlator = null,
|
||||
read = chapter.read,
|
||||
bookmark = chapter.bookmark,
|
||||
lastPageRead = chapter.lastPageRead,
|
||||
chapterNumber = null,
|
||||
sourceOrder = null,
|
||||
dateFetch = null,
|
||||
dateUpload = null,
|
||||
chapterId = chapter.id,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches manga information
|
||||
*
|
||||
* @param manga manga that needs updating
|
||||
* @return Updated manga info.
|
||||
*/
|
||||
private suspend fun restoreNewManga(manga: Manga): Manga {
|
||||
return manga.copy(
|
||||
initialized = manga.description != null,
|
||||
id = insertManga(manga),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts manga and returns id
|
||||
*
|
||||
* @return id of [Manga], null if not found
|
||||
*/
|
||||
private suspend fun insertManga(manga: Manga): Long {
|
||||
return mangaHandler.awaitOneExecutable(true) {
|
||||
mangasQueries.insert(
|
||||
source = manga.source,
|
||||
url = manga.url,
|
||||
artist = manga.artist,
|
||||
author = manga.author,
|
||||
description = manga.description,
|
||||
genre = manga.genre,
|
||||
title = manga.title,
|
||||
status = manga.status.toLong(),
|
||||
thumbnailUrl = manga.thumbnailUrl,
|
||||
favorite = manga.favorite,
|
||||
lastUpdate = manga.lastUpdate,
|
||||
nextUpdate = 0L,
|
||||
calculateInterval = 0L,
|
||||
initialized = manga.initialized,
|
||||
viewerFlags = manga.viewerFlags,
|
||||
chapterFlags = manga.chapterFlags,
|
||||
coverLastModified = manga.coverLastModified,
|
||||
dateAdded = manga.dateAdded,
|
||||
updateStrategy = manga.updateStrategy,
|
||||
)
|
||||
mangasQueries.selectLastInsertedRowId()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun restoreNewManga(
|
||||
backupManga: Manga,
|
||||
chapters: List<Chapter>,
|
||||
|
@ -301,7 +552,7 @@ class BackupRestorer(
|
|||
tracks: List<MangaTrack>,
|
||||
backupCategories: List<BackupCategory>,
|
||||
): Manga {
|
||||
backupManager.restoreChapters(backupManga, chapters)
|
||||
restoreChapters(backupManga, chapters)
|
||||
restoreExtras(backupManga, categories, history, tracks, backupCategories)
|
||||
return backupManga
|
||||
}
|
||||
|
@ -313,9 +564,186 @@ class BackupRestorer(
|
|||
tracks: List<MangaTrack>,
|
||||
backupCategories: List<BackupCategory>,
|
||||
) {
|
||||
backupManager.restoreCategories(manga, categories, backupCategories)
|
||||
backupManager.restoreHistory(history)
|
||||
backupManager.restoreTracking(manga, tracks)
|
||||
restoreCategories(manga, categories, backupCategories)
|
||||
restoreHistory(history)
|
||||
restoreTracking(manga, tracks)
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the categories a manga is in.
|
||||
*
|
||||
* @param manga the manga whose categories have to be restored.
|
||||
* @param categories the categories to restore.
|
||||
*/
|
||||
private suspend fun restoreCategories(
|
||||
manga: Manga,
|
||||
categories: List<Int>,
|
||||
backupCategories: List<BackupCategory>,
|
||||
) {
|
||||
val dbCategories = getMangaCategories.await()
|
||||
val mangaCategoriesToUpdate = mutableListOf<Pair<Long, Long>>()
|
||||
|
||||
categories.forEach { backupCategoryOrder ->
|
||||
backupCategories.firstOrNull {
|
||||
it.order == backupCategoryOrder.toLong()
|
||||
}?.let { backupCategory ->
|
||||
dbCategories.firstOrNull { dbCategory ->
|
||||
dbCategory.name == backupCategory.name
|
||||
}?.let { dbCategory ->
|
||||
mangaCategoriesToUpdate.add(Pair(manga.id, dbCategory.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update database
|
||||
if (mangaCategoriesToUpdate.isNotEmpty()) {
|
||||
mangaHandler.await(true) {
|
||||
mangas_categoriesQueries.deleteMangaCategoryByMangaId(manga.id)
|
||||
mangaCategoriesToUpdate.forEach { (mangaId, categoryId) ->
|
||||
mangas_categoriesQueries.insert(mangaId, categoryId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore history from Json
|
||||
*
|
||||
* @param history list containing history to be restored
|
||||
*/
|
||||
private suspend fun restoreHistory(history: List<BackupHistory>) {
|
||||
// List containing history to be updated
|
||||
val toUpdate = mutableListOf<MangaHistoryUpdate>()
|
||||
for ((url, lastRead, readDuration) in history) {
|
||||
var dbHistory = mangaHandler.awaitOneOrNull { historyQueries.getHistoryByChapterUrl(url) }
|
||||
// Check if history already in database and update
|
||||
if (dbHistory != null) {
|
||||
dbHistory = dbHistory.copy(
|
||||
last_read = Date(max(lastRead, dbHistory.last_read?.time ?: 0L)),
|
||||
time_read = max(readDuration, dbHistory.time_read) - dbHistory.time_read,
|
||||
)
|
||||
toUpdate.add(
|
||||
MangaHistoryUpdate(
|
||||
chapterId = dbHistory.chapter_id,
|
||||
readAt = dbHistory.last_read!!,
|
||||
sessionReadDuration = dbHistory.time_read,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
// If not in database create
|
||||
mangaHandler
|
||||
.awaitOneOrNull { chaptersQueries.getChapterByUrl(url) }
|
||||
?.let {
|
||||
toUpdate.add(
|
||||
MangaHistoryUpdate(
|
||||
chapterId = it._id,
|
||||
readAt = Date(lastRead),
|
||||
sessionReadDuration = readDuration,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
mangaHandler.await(true) {
|
||||
toUpdate.forEach { payload ->
|
||||
historyQueries.upsert(
|
||||
payload.chapterId,
|
||||
payload.readAt,
|
||||
payload.sessionReadDuration,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the sync of a manga.
|
||||
*
|
||||
* @param manga the manga whose sync have to be restored.
|
||||
* @param tracks the track list to restore.
|
||||
*/
|
||||
private suspend fun restoreTracking(
|
||||
manga: Manga,
|
||||
tracks: List<tachiyomi.domain.track.manga.model.MangaTrack>,
|
||||
) {
|
||||
// Get tracks from database
|
||||
val dbTracks = mangaHandler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id) }
|
||||
val toUpdate = mutableListOf<Manga_sync>()
|
||||
val toInsert = mutableListOf<tachiyomi.domain.track.manga.model.MangaTrack>()
|
||||
|
||||
tracks
|
||||
// Fix foreign keys with the current manga id
|
||||
.map { it.copy(mangaId = manga.id) }
|
||||
.forEach { track ->
|
||||
var isInDatabase = false
|
||||
for (dbTrack in dbTracks) {
|
||||
if (track.syncId == dbTrack.sync_id) {
|
||||
// The sync is already in the db, only update its fields
|
||||
var temp = dbTrack
|
||||
if (track.remoteId != dbTrack.remote_id) {
|
||||
temp = temp.copy(remote_id = track.remoteId)
|
||||
}
|
||||
if (track.libraryId != dbTrack.library_id) {
|
||||
temp = temp.copy(library_id = track.libraryId)
|
||||
}
|
||||
temp = temp.copy(
|
||||
last_chapter_read = max(
|
||||
dbTrack.last_chapter_read,
|
||||
track.lastChapterRead,
|
||||
),
|
||||
)
|
||||
isInDatabase = true
|
||||
toUpdate.add(temp)
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!isInDatabase) {
|
||||
// Insert new sync. Let the db assign the id
|
||||
toInsert.add(track.copy(id = 0))
|
||||
}
|
||||
}
|
||||
|
||||
// Update database
|
||||
if (toUpdate.isNotEmpty()) {
|
||||
mangaHandler.await(true) {
|
||||
toUpdate.forEach { track ->
|
||||
manga_syncQueries.update(
|
||||
track.manga_id,
|
||||
track.sync_id,
|
||||
track.remote_id,
|
||||
track.library_id,
|
||||
track.title,
|
||||
track.last_chapter_read,
|
||||
track.total_chapters,
|
||||
track.status,
|
||||
track.score,
|
||||
track.remote_url,
|
||||
track.start_date,
|
||||
track.finish_date,
|
||||
track._id,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (toInsert.isNotEmpty()) {
|
||||
mangaHandler.await(true) {
|
||||
toInsert.forEach { track ->
|
||||
manga_syncQueries.insert(
|
||||
track.mangaId,
|
||||
track.syncId,
|
||||
track.remoteId,
|
||||
track.libraryId,
|
||||
track.title,
|
||||
track.lastChapterRead,
|
||||
track.totalChapters,
|
||||
track.status,
|
||||
track.score,
|
||||
track.remoteUrl,
|
||||
track.startDate,
|
||||
track.finishDate,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun restoreAnime(
|
||||
|
@ -331,14 +759,14 @@ class BackupRestorer(
|
|||
val tracks = backupAnime.getTrackingImpl()
|
||||
|
||||
try {
|
||||
val dbAnime = backupManager.getAnimeFromDatabase(anime.url, anime.source)
|
||||
val dbAnime = getAnimeFromDatabase(anime.url, anime.source)
|
||||
val restoredAnime = if (dbAnime == null) {
|
||||
// Anime not in database
|
||||
restoreExistingAnime(anime, episodes, categories, history, tracks, backupCategories)
|
||||
} else {
|
||||
// Anime in database
|
||||
// Copy information from anime already in database
|
||||
val updateAnime = backupManager.restoreExistingAnime(anime, dbAnime)
|
||||
val updateAnime = restoreExistingAnime(anime, dbAnime)
|
||||
// Fetch rest of anime information
|
||||
restoreNewAnime(
|
||||
updateAnime,
|
||||
|
@ -373,6 +801,50 @@ class BackupRestorer(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns anime
|
||||
*
|
||||
* @return [Anime], null if not found
|
||||
*/
|
||||
private suspend fun getAnimeFromDatabase(url: String, source: Long): Animes? {
|
||||
return animeHandler.awaitOneOrNull { animesQueries.getAnimeByUrlAndSource(url, source) }
|
||||
}
|
||||
|
||||
private suspend fun restoreExistingAnime(anime: Anime, dbAnime: Animes): Anime {
|
||||
var updatedAnime = anime.copy(id = dbAnime._id)
|
||||
updatedAnime = updatedAnime.copyFrom(dbAnime)
|
||||
updateAnime(updatedAnime)
|
||||
return updatedAnime
|
||||
}
|
||||
|
||||
private suspend fun updateAnime(anime: Anime): Long {
|
||||
animeHandler.await(true) {
|
||||
animesQueries.update(
|
||||
source = anime.source,
|
||||
url = anime.url,
|
||||
artist = anime.artist,
|
||||
author = anime.author,
|
||||
description = anime.description,
|
||||
genre = anime.genre?.joinToString(separator = ", "),
|
||||
title = anime.title,
|
||||
status = anime.status,
|
||||
thumbnailUrl = anime.thumbnailUrl,
|
||||
favorite = anime.favorite,
|
||||
lastUpdate = anime.lastUpdate,
|
||||
nextUpdate = null,
|
||||
calculateInterval = null,
|
||||
initialized = anime.initialized,
|
||||
viewer = anime.viewerFlags,
|
||||
episodeFlags = anime.episodeFlags,
|
||||
coverLastModified = anime.coverLastModified,
|
||||
dateAdded = anime.dateAdded,
|
||||
animeId = anime.id!!,
|
||||
updateStrategy = anime.updateStrategy.let(UpdateStrategyColumnAdapter::encode),
|
||||
)
|
||||
}
|
||||
return anime.id
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches anime information
|
||||
*
|
||||
|
@ -388,12 +860,143 @@ class BackupRestorer(
|
|||
tracks: List<AnimeTrack>,
|
||||
backupCategories: List<BackupCategory>,
|
||||
): Anime {
|
||||
val fetchedAnime = backupManager.restoreNewAnime(anime)
|
||||
backupManager.restoreEpisodes(fetchedAnime, episodes)
|
||||
val fetchedAnime = restoreNewAnime(anime)
|
||||
restoreEpisodes(fetchedAnime, episodes)
|
||||
restoreExtras(fetchedAnime, categories, history, tracks, backupCategories)
|
||||
return fetchedAnime
|
||||
}
|
||||
|
||||
private suspend fun restoreEpisodes(
|
||||
anime: Anime,
|
||||
episodes: List<tachiyomi.domain.items.episode.model.Episode>,
|
||||
) {
|
||||
val dbEpisodes = animeHandler.awaitList { episodesQueries.getEpisodesByAnimeId(anime.id!!) }
|
||||
|
||||
val processed = episodes.map { episode ->
|
||||
var updatedEpisode = episode
|
||||
val dbEpisode = dbEpisodes.find { it.url == updatedEpisode.url }
|
||||
if (dbEpisode != null) {
|
||||
updatedEpisode = updatedEpisode.copy(id = dbEpisode._id)
|
||||
updatedEpisode = updatedEpisode.copyFrom(dbEpisode)
|
||||
if (dbEpisode.seen && !updatedEpisode.seen) {
|
||||
updatedEpisode = updatedEpisode.copy(
|
||||
seen = true,
|
||||
lastSecondSeen = dbEpisode.last_second_seen,
|
||||
)
|
||||
} else if (updatedEpisode.lastSecondSeen == 0L && dbEpisode.last_second_seen != 0L) {
|
||||
updatedEpisode = updatedEpisode.copy(
|
||||
lastSecondSeen = dbEpisode.last_second_seen,
|
||||
)
|
||||
}
|
||||
if (!updatedEpisode.bookmark && dbEpisode.bookmark) {
|
||||
updatedEpisode = updatedEpisode.copy(bookmark = true)
|
||||
}
|
||||
}
|
||||
|
||||
updatedEpisode.copy(animeId = anime.id ?: -1)
|
||||
}
|
||||
|
||||
val newEpisodes = processed.groupBy { it.id > 0 }
|
||||
newEpisodes[true]?.let { updateKnownEpisodes(it) }
|
||||
newEpisodes[false]?.let { insertEpisodes(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts list of episodes
|
||||
*/
|
||||
private suspend fun insertEpisodes(episodes: List<tachiyomi.domain.items.episode.model.Episode>) {
|
||||
animeHandler.await(true) {
|
||||
episodes.forEach { episode ->
|
||||
episodesQueries.insert(
|
||||
episode.animeId,
|
||||
episode.url,
|
||||
episode.name,
|
||||
episode.scanlator,
|
||||
episode.seen,
|
||||
episode.bookmark,
|
||||
episode.lastSecondSeen,
|
||||
episode.totalSeconds,
|
||||
episode.episodeNumber,
|
||||
episode.sourceOrder,
|
||||
episode.dateFetch,
|
||||
episode.dateUpload,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a list of episodes with known database ids
|
||||
*/
|
||||
private suspend fun updateKnownEpisodes(
|
||||
episodes: List<tachiyomi.domain.items.episode.model.Episode>,
|
||||
) {
|
||||
animeHandler.await(true) {
|
||||
episodes.forEach { episode ->
|
||||
episodesQueries.update(
|
||||
animeId = null,
|
||||
url = null,
|
||||
name = null,
|
||||
scanlator = null,
|
||||
seen = episode.seen,
|
||||
bookmark = episode.bookmark,
|
||||
lastSecondSeen = episode.lastSecondSeen,
|
||||
totalSeconds = episode.totalSeconds,
|
||||
episodeNumber = null,
|
||||
sourceOrder = null,
|
||||
dateFetch = null,
|
||||
dateUpload = null,
|
||||
episodeId = episode.id,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches anime information
|
||||
*
|
||||
* @param anime anime that needs updating
|
||||
* @return Updated anime info.
|
||||
*/
|
||||
private suspend fun restoreNewAnime(anime: Anime): Anime {
|
||||
return anime.copy(
|
||||
initialized = anime.description != null,
|
||||
id = insertAnime(anime),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts anime and returns id
|
||||
*
|
||||
* @return id of [Anime], null if not found
|
||||
*/
|
||||
private suspend fun insertAnime(anime: Anime): Long {
|
||||
return animeHandler.awaitOneExecutable(true) {
|
||||
animesQueries.insert(
|
||||
source = anime.source,
|
||||
url = anime.url,
|
||||
artist = anime.artist,
|
||||
author = anime.author,
|
||||
description = anime.description,
|
||||
genre = anime.genre,
|
||||
title = anime.title,
|
||||
status = anime.status.toLong(),
|
||||
thumbnailUrl = anime.thumbnailUrl,
|
||||
favorite = anime.favorite,
|
||||
lastUpdate = anime.lastUpdate,
|
||||
nextUpdate = 0L,
|
||||
calculateInterval = 0L,
|
||||
initialized = anime.initialized,
|
||||
viewerFlags = anime.viewerFlags,
|
||||
episodeFlags = anime.episodeFlags,
|
||||
coverLastModified = anime.coverLastModified,
|
||||
dateAdded = anime.dateAdded,
|
||||
updateStrategy = anime.updateStrategy,
|
||||
)
|
||||
animesQueries.selectLastInsertedRowId()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun restoreNewAnime(
|
||||
backupAnime: Anime,
|
||||
episodes: List<Episode>,
|
||||
|
@ -402,7 +1005,7 @@ class BackupRestorer(
|
|||
tracks: List<AnimeTrack>,
|
||||
backupCategories: List<BackupCategory>,
|
||||
): Anime {
|
||||
backupManager.restoreEpisodes(backupAnime, episodes)
|
||||
restoreEpisodes(backupAnime, episodes)
|
||||
restoreExtras(backupAnime, categories, history, tracks, backupCategories)
|
||||
return backupAnime
|
||||
}
|
||||
|
@ -414,9 +1017,186 @@ class BackupRestorer(
|
|||
tracks: List<AnimeTrack>,
|
||||
backupCategories: List<BackupCategory>,
|
||||
) {
|
||||
backupManager.restoreAnimeCategories(anime, categories, backupCategories)
|
||||
backupManager.restoreAnimeHistory(history)
|
||||
backupManager.restoreAnimeTracking(anime, tracks)
|
||||
restoreAnimeCategories(anime, categories, backupCategories)
|
||||
restoreAnimeHistory(history)
|
||||
restoreAnimeTracking(anime, tracks)
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the categories an anime is in.
|
||||
*
|
||||
* @param anime the anime whose categories have to be restored.
|
||||
* @param categories the categories to restore.
|
||||
*/
|
||||
private suspend fun restoreAnimeCategories(
|
||||
anime: Anime,
|
||||
categories: List<Int>,
|
||||
backupCategories: List<BackupCategory>,
|
||||
) {
|
||||
val dbCategories = getAnimeCategories.await()
|
||||
val animeCategoriesToUpdate = mutableListOf<Pair<Long, Long>>()
|
||||
|
||||
categories.forEach { backupCategoryOrder ->
|
||||
backupCategories.firstOrNull {
|
||||
it.order == backupCategoryOrder.toLong()
|
||||
}?.let { backupCategory ->
|
||||
dbCategories.firstOrNull { dbCategory ->
|
||||
dbCategory.name == backupCategory.name
|
||||
}?.let { dbCategory ->
|
||||
animeCategoriesToUpdate.add(Pair(anime.id, dbCategory.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update database
|
||||
if (animeCategoriesToUpdate.isNotEmpty()) {
|
||||
animeHandler.await(true) {
|
||||
animes_categoriesQueries.deleteAnimeCategoryByAnimeId(anime.id)
|
||||
animeCategoriesToUpdate.forEach { (animeId, categoryId) ->
|
||||
animes_categoriesQueries.insert(animeId, categoryId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore history from Json
|
||||
*
|
||||
* @param history list containing history to be restored
|
||||
*/
|
||||
private suspend fun restoreAnimeHistory(history: List<BackupAnimeHistory>) {
|
||||
// List containing history to be updated
|
||||
val toUpdate = mutableListOf<AnimeHistoryUpdate>()
|
||||
for ((url, lastSeen) in history) {
|
||||
var dbHistory = animeHandler.awaitOneOrNull {
|
||||
animehistoryQueries.getHistoryByEpisodeUrl(
|
||||
url,
|
||||
)
|
||||
}
|
||||
// Check if history already in database and update
|
||||
if (dbHistory != null) {
|
||||
dbHistory = dbHistory.copy(
|
||||
last_seen = Date(max(lastSeen, dbHistory.last_seen?.time ?: 0L)),
|
||||
)
|
||||
toUpdate.add(
|
||||
AnimeHistoryUpdate(
|
||||
episodeId = dbHistory.episode_id,
|
||||
seenAt = dbHistory.last_seen!!,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
// If not in database create
|
||||
animeHandler
|
||||
.awaitOneOrNull { episodesQueries.getEpisodeByUrl(url) }
|
||||
?.let {
|
||||
toUpdate.add(
|
||||
AnimeHistoryUpdate(
|
||||
episodeId = it._id,
|
||||
seenAt = Date(lastSeen),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
animeHandler.await(true) {
|
||||
toUpdate.forEach { payload ->
|
||||
animehistoryQueries.upsert(
|
||||
payload.episodeId,
|
||||
payload.seenAt,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the sync of a manga.
|
||||
*
|
||||
* @param anime the anime whose sync have to be restored.
|
||||
* @param tracks the track list to restore.
|
||||
*/
|
||||
private suspend fun restoreAnimeTracking(
|
||||
anime: Anime,
|
||||
tracks: List<tachiyomi.domain.track.anime.model.AnimeTrack>,
|
||||
) {
|
||||
// Get tracks from database
|
||||
val dbTracks = animeHandler.awaitList { anime_syncQueries.getTracksByAnimeId(anime.id) }
|
||||
val toUpdate = mutableListOf<Anime_sync>()
|
||||
val toInsert = mutableListOf<tachiyomi.domain.track.anime.model.AnimeTrack>()
|
||||
|
||||
tracks
|
||||
// Fix foreign keys with the current manga id
|
||||
.map { it.copy(animeId = anime.id) }
|
||||
.forEach { track ->
|
||||
var isInDatabase = false
|
||||
for (dbTrack in dbTracks) {
|
||||
if (track.syncId == dbTrack.sync_id) {
|
||||
// The sync is already in the db, only update its fields
|
||||
var temp = dbTrack
|
||||
if (track.remoteId != dbTrack.remote_id) {
|
||||
temp = temp.copy(remote_id = track.remoteId)
|
||||
}
|
||||
if (track.libraryId != dbTrack.library_id) {
|
||||
temp = temp.copy(library_id = track.libraryId)
|
||||
}
|
||||
temp = temp.copy(
|
||||
last_episode_seen = max(
|
||||
dbTrack.last_episode_seen,
|
||||
track.lastEpisodeSeen,
|
||||
),
|
||||
)
|
||||
isInDatabase = true
|
||||
toUpdate.add(temp)
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!isInDatabase) {
|
||||
// Insert new sync. Let the db assign the id
|
||||
toInsert.add(track.copy(id = 0))
|
||||
}
|
||||
}
|
||||
|
||||
// Update database
|
||||
if (toUpdate.isNotEmpty()) {
|
||||
animeHandler.await(true) {
|
||||
toUpdate.forEach { track ->
|
||||
anime_syncQueries.update(
|
||||
track.anime_id,
|
||||
track.sync_id,
|
||||
track.remote_id,
|
||||
track.library_id,
|
||||
track.title,
|
||||
track.last_episode_seen,
|
||||
track.total_episodes,
|
||||
track.status,
|
||||
track.score,
|
||||
track.remote_url,
|
||||
track.start_date,
|
||||
track.finish_date,
|
||||
track._id,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (toInsert.isNotEmpty()) {
|
||||
animeHandler.await(true) {
|
||||
toInsert.forEach { track ->
|
||||
anime_syncQueries.insert(
|
||||
track.animeId,
|
||||
track.syncId,
|
||||
track.remoteId,
|
||||
track.libraryId,
|
||||
track.title,
|
||||
track.lastEpisodeSeen,
|
||||
track.totalEpisodes,
|
||||
track.status,
|
||||
track.score,
|
||||
track.remoteUrl,
|
||||
track.startDate,
|
||||
track.finishDate,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun restorePreferences(
|
||||
|
@ -464,6 +1244,9 @@ class BackupRestorer(
|
|||
val sharedPrefs = context.getSharedPreferences(it.name, 0x0)
|
||||
restorePreferences(it.prefs, sharedPrefs)
|
||||
}
|
||||
|
||||
restoreProgress += 1
|
||||
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.extension_settings), context.getString(R.string.restoring_backup))
|
||||
}
|
||||
|
||||
private fun restoreExtensions(extensions: List<BackupExtension>) {
|
||||
|
|
|
@ -186,7 +186,40 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
.distinctBy { it.anime.id }
|
||||
}
|
||||
|
||||
val restrictions = libraryPreferences.autoUpdateItemRestrictions().get()
|
||||
val skippedUpdates = mutableListOf<Pair<Anime, String?>>()
|
||||
val fetchWindow = animeFetchInterval.getWindow(ZonedDateTime.now())
|
||||
|
||||
animeToUpdate = listToUpdate
|
||||
.filter {
|
||||
when {
|
||||
it.anime.updateStrategy != UpdateStrategy.ALWAYS_UPDATE -> {
|
||||
skippedUpdates.add(it.anime to context.getString(R.string.skipped_reason_not_always_update))
|
||||
false
|
||||
}
|
||||
|
||||
ENTRY_NON_COMPLETED in restrictions && it.anime.status.toInt() == SAnime.COMPLETED -> {
|
||||
skippedUpdates.add(it.anime to context.getString(R.string.skipped_reason_completed))
|
||||
false
|
||||
}
|
||||
|
||||
ENTRY_HAS_UNVIEWED in restrictions && it.unseenCount != 0L -> {
|
||||
skippedUpdates.add(it.anime to context.getString(R.string.skipped_reason_not_caught_up))
|
||||
false
|
||||
}
|
||||
|
||||
ENTRY_NON_VIEWED in restrictions && it.totalEpisodes > 0L && !it.hasStarted -> {
|
||||
skippedUpdates.add(it.anime to context.getString(R.string.skipped_reason_not_started))
|
||||
false
|
||||
}
|
||||
|
||||
ENTRY_OUTSIDE_RELEASE_PERIOD in restrictions && it.anime.nextUpdate > fetchWindow.second -> {
|
||||
skippedUpdates.add(it.anime to context.getString(R.string.skipped_reason_not_in_release_period))
|
||||
false
|
||||
}
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
.sortedBy { it.anime.title }
|
||||
|
||||
// Warn when excessively checking a single source
|
||||
|
@ -197,6 +230,17 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
if (maxUpdatesFromSource > ANIME_PER_SOURCE_QUEUE_WARNING_THRESHOLD) {
|
||||
notifier.showQueueSizeWarningNotification()
|
||||
}
|
||||
|
||||
if (skippedUpdates.isNotEmpty()) {
|
||||
// TODO: surface skipped reasons to user?
|
||||
logcat {
|
||||
skippedUpdates
|
||||
.groupBy { it.second }
|
||||
.map { (reason, entries) -> "$reason: [${entries.map { it.first.title }.sorted().joinToString()}]" }
|
||||
.joinToString()
|
||||
}
|
||||
notifier.showUpdateSkippedNotification(skippedUpdates.size)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -212,10 +256,8 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
val progressCount = AtomicInteger(0)
|
||||
val currentlyUpdatingAnime = CopyOnWriteArrayList<Anime>()
|
||||
val newUpdates = CopyOnWriteArrayList<Pair<Anime, Array<Episode>>>()
|
||||
val skippedUpdates = CopyOnWriteArrayList<Pair<Anime, String?>>()
|
||||
val failedUpdates = CopyOnWriteArrayList<Pair<Anime, String?>>()
|
||||
val hasDownloads = AtomicBoolean(false)
|
||||
val restrictions = libraryPreferences.autoUpdateItemRestrictions().get()
|
||||
val fetchWindow = animeFetchInterval.getWindow(ZonedDateTime.now())
|
||||
|
||||
coroutineScope {
|
||||
|
@ -237,79 +279,30 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
progressCount,
|
||||
anime,
|
||||
) {
|
||||
when {
|
||||
anime.updateStrategy != UpdateStrategy.ALWAYS_UPDATE ->
|
||||
skippedUpdates.add(
|
||||
anime to context.getString(
|
||||
R.string.skipped_reason_not_always_update,
|
||||
),
|
||||
)
|
||||
try {
|
||||
val newChapters = updateAnime(anime, fetchWindow)
|
||||
.sortedByDescending { it.sourceOrder }
|
||||
|
||||
ENTRY_NON_COMPLETED in restrictions && anime.status.toInt() == SAnime.COMPLETED ->
|
||||
skippedUpdates.add(
|
||||
anime to context.getString(
|
||||
R.string.skipped_reason_completed,
|
||||
),
|
||||
)
|
||||
|
||||
ENTRY_HAS_UNVIEWED in restrictions && libraryAnime.unseenCount != 0L ->
|
||||
skippedUpdates.add(
|
||||
anime to context.getString(
|
||||
R.string.skipped_reason_not_caught_up,
|
||||
),
|
||||
)
|
||||
|
||||
ENTRY_NON_VIEWED in restrictions && libraryAnime.totalEpisodes > 0L && !libraryAnime.hasStarted ->
|
||||
skippedUpdates.add(
|
||||
anime to context.getString(
|
||||
R.string.skipped_reason_not_started,
|
||||
),
|
||||
)
|
||||
|
||||
ENTRY_OUTSIDE_RELEASE_PERIOD in restrictions && anime.nextUpdate > fetchWindow.second ->
|
||||
skippedUpdates.add(
|
||||
anime to context.getString(
|
||||
R.string.skipped_reason_not_in_release_period,
|
||||
),
|
||||
)
|
||||
|
||||
else -> {
|
||||
try {
|
||||
val newEpisodes = updateAnime(anime, fetchWindow)
|
||||
.sortedByDescending { it.sourceOrder }
|
||||
|
||||
if (newEpisodes.isNotEmpty()) {
|
||||
val categoryIds = getCategories.await(anime.id).map { it.id }
|
||||
if (anime.shouldDownloadNewEpisodes(
|
||||
categoryIds,
|
||||
downloadPreferences,
|
||||
)
|
||||
) {
|
||||
downloadEpisodes(anime, newEpisodes)
|
||||
hasDownloads.set(true)
|
||||
}
|
||||
|
||||
libraryPreferences.newAnimeUpdatesCount().getAndSet { it + newEpisodes.size }
|
||||
|
||||
// Convert to the anime that contains new chapters
|
||||
newUpdates.add(
|
||||
anime to newEpisodes.toTypedArray(),
|
||||
)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
val errorMessage = when (e) {
|
||||
is NoEpisodesException -> context.getString(
|
||||
R.string.no_episodes_error,
|
||||
)
|
||||
// failedUpdates will already have the source, don't need to copy it into the message
|
||||
is AnimeSourceNotInstalledException -> context.getString(
|
||||
R.string.loader_not_implemented_error,
|
||||
)
|
||||
else -> e.message
|
||||
}
|
||||
failedUpdates.add(anime to errorMessage)
|
||||
if (newChapters.isNotEmpty()) {
|
||||
val categoryIds = getCategories.await(anime.id).map { it.id }
|
||||
if (anime.shouldDownloadNewEpisodes(categoryIds, downloadPreferences)) {
|
||||
downloadEpisodes(anime, newChapters)
|
||||
hasDownloads.set(true)
|
||||
}
|
||||
|
||||
libraryPreferences.newAnimeUpdatesCount().getAndSet { it + newChapters.size }
|
||||
|
||||
// Convert to the manga that contains new chapters
|
||||
newUpdates.add(anime to newChapters.toTypedArray())
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
val errorMessage = when (e) {
|
||||
is NoEpisodesException -> context.getString(R.string.no_chapters_error)
|
||||
// failedUpdates will already have the source, don't need to copy it into the message
|
||||
is AnimeSourceNotInstalledException -> context.getString(R.string.loader_not_implemented_error)
|
||||
else -> e.message
|
||||
}
|
||||
failedUpdates.add(anime to errorMessage)
|
||||
}
|
||||
|
||||
if (libraryPreferences.autoUpdateTrackers().get()) {
|
||||
|
@ -339,16 +332,6 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
errorFile.getUriCompat(context),
|
||||
)
|
||||
}
|
||||
if (skippedUpdates.isNotEmpty()) {
|
||||
// TODO: surface skipped reasons to user
|
||||
logcat {
|
||||
skippedUpdates
|
||||
.groupBy { it.second }
|
||||
.map { (reason, entries) -> "$reason: [${entries.map { it.first.title }.sorted().joinToString()}]" }
|
||||
.joinToString()
|
||||
}
|
||||
notifier.showUpdateSkippedNotification(skippedUpdates.size)
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadEpisodes(anime: Anime, episodes: List<Episode>) {
|
||||
|
@ -466,29 +449,27 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
completed: AtomicInteger,
|
||||
anime: Anime,
|
||||
block: suspend () -> Unit,
|
||||
) {
|
||||
coroutineScope {
|
||||
ensureActive()
|
||||
) = coroutineScope {
|
||||
ensureActive()
|
||||
|
||||
updatingAnime.add(anime)
|
||||
notifier.showProgressNotification(
|
||||
updatingAnime,
|
||||
completed.get(),
|
||||
animeToUpdate.size,
|
||||
)
|
||||
updatingAnime.add(anime)
|
||||
notifier.showProgressNotification(
|
||||
updatingAnime,
|
||||
completed.get(),
|
||||
animeToUpdate.size,
|
||||
)
|
||||
|
||||
block()
|
||||
block()
|
||||
|
||||
ensureActive()
|
||||
ensureActive()
|
||||
|
||||
updatingAnime.remove(anime)
|
||||
completed.getAndIncrement()
|
||||
notifier.showProgressNotification(
|
||||
updatingAnime,
|
||||
completed.get(),
|
||||
animeToUpdate.size,
|
||||
)
|
||||
}
|
||||
updatingAnime.remove(anime)
|
||||
completed.getAndIncrement()
|
||||
notifier.showProgressNotification(
|
||||
updatingAnime,
|
||||
completed.get(),
|
||||
animeToUpdate.size,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -30,10 +30,14 @@ import tachiyomi.core.util.lang.launchUI
|
|||
import tachiyomi.domain.entries.anime.model.Anime
|
||||
import tachiyomi.domain.items.episode.model.Episode
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.NumberFormat
|
||||
|
||||
class AnimeLibraryUpdateNotifier(private val context: Context) {
|
||||
|
||||
private val preferences: SecurityPreferences by injectLazy()
|
||||
private val percentFormatter = NumberFormat.getPercentInstance().apply {
|
||||
maximumFractionDigits = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Pending intent of action that cancels the library update
|
||||
|
@ -82,7 +86,7 @@ class AnimeLibraryUpdateNotifier(private val context: Context) {
|
|||
} else {
|
||||
val updatingText = anime.joinToString("\n") { it.title.chop(40) }
|
||||
progressNotificationBuilder
|
||||
.setContentTitle(context.getString(R.string.notification_updating, current, total))
|
||||
.setContentTitle(context.getString(R.string.notification_updating_progress, percentFormatter.format(current.toFloat() / total)))
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(updatingText))
|
||||
}
|
||||
|
||||
|
|
|
@ -186,7 +186,40 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
.distinctBy { it.manga.id }
|
||||
}
|
||||
|
||||
val restrictions = libraryPreferences.autoUpdateItemRestrictions().get()
|
||||
val skippedUpdates = mutableListOf<Pair<Manga, String?>>()
|
||||
val fetchWindow = mangaFetchInterval.getWindow(ZonedDateTime.now())
|
||||
|
||||
mangaToUpdate = listToUpdate
|
||||
.filter {
|
||||
when {
|
||||
it.manga.updateStrategy != UpdateStrategy.ALWAYS_UPDATE -> {
|
||||
skippedUpdates.add(it.manga to context.getString(R.string.skipped_reason_not_always_update))
|
||||
false
|
||||
}
|
||||
|
||||
ENTRY_NON_COMPLETED in restrictions && it.manga.status.toInt() == SManga.COMPLETED -> {
|
||||
skippedUpdates.add(it.manga to context.getString(R.string.skipped_reason_completed))
|
||||
false
|
||||
}
|
||||
|
||||
ENTRY_HAS_UNVIEWED in restrictions && it.unreadCount != 0L -> {
|
||||
skippedUpdates.add(it.manga to context.getString(R.string.skipped_reason_not_caught_up))
|
||||
false
|
||||
}
|
||||
|
||||
ENTRY_NON_VIEWED in restrictions && it.totalChapters > 0L && !it.hasStarted -> {
|
||||
skippedUpdates.add(it.manga to context.getString(R.string.skipped_reason_not_started))
|
||||
false
|
||||
}
|
||||
|
||||
ENTRY_OUTSIDE_RELEASE_PERIOD in restrictions && it.manga.nextUpdate > fetchWindow.second -> {
|
||||
skippedUpdates.add(it.manga to context.getString(R.string.skipped_reason_not_in_release_period))
|
||||
false
|
||||
}
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
.sortedBy { it.manga.title }
|
||||
|
||||
// Warn when excessively checking a single source
|
||||
|
@ -197,6 +230,17 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
if (maxUpdatesFromSource > MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD) {
|
||||
notifier.showQueueSizeWarningNotification()
|
||||
}
|
||||
|
||||
if (skippedUpdates.isNotEmpty()) {
|
||||
// TODO: surface skipped reasons to user?
|
||||
logcat {
|
||||
skippedUpdates
|
||||
.groupBy { it.second }
|
||||
.map { (reason, entries) -> "$reason: [${entries.map { it.first.title }.sorted().joinToString()}]" }
|
||||
.joinToString()
|
||||
}
|
||||
notifier.showUpdateSkippedNotification(skippedUpdates.size)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -212,10 +256,8 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
val progressCount = AtomicInteger(0)
|
||||
val currentlyUpdatingManga = CopyOnWriteArrayList<Manga>()
|
||||
val newUpdates = CopyOnWriteArrayList<Pair<Manga, Array<Chapter>>>()
|
||||
val skippedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
||||
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
||||
val hasDownloads = AtomicBoolean(false)
|
||||
val restrictions = libraryPreferences.autoUpdateItemRestrictions().get()
|
||||
val fetchWindow = mangaFetchInterval.getWindow(ZonedDateTime.now())
|
||||
|
||||
coroutineScope {
|
||||
|
@ -237,79 +279,29 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
progressCount,
|
||||
manga,
|
||||
) {
|
||||
when {
|
||||
manga.updateStrategy != UpdateStrategy.ALWAYS_UPDATE ->
|
||||
skippedUpdates.add(
|
||||
manga to context.getString(
|
||||
R.string.skipped_reason_not_always_update,
|
||||
),
|
||||
)
|
||||
try {
|
||||
val newChapters = updateManga(manga, fetchWindow)
|
||||
.sortedByDescending { it.sourceOrder }
|
||||
|
||||
ENTRY_NON_COMPLETED in restrictions && manga.status.toInt() == SManga.COMPLETED ->
|
||||
skippedUpdates.add(
|
||||
manga to context.getString(
|
||||
R.string.skipped_reason_completed,
|
||||
),
|
||||
)
|
||||
|
||||
ENTRY_HAS_UNVIEWED in restrictions && libraryManga.unreadCount != 0L ->
|
||||
skippedUpdates.add(
|
||||
manga to context.getString(
|
||||
R.string.skipped_reason_not_caught_up,
|
||||
),
|
||||
)
|
||||
|
||||
ENTRY_NON_VIEWED in restrictions && libraryManga.totalChapters > 0L && !libraryManga.hasStarted ->
|
||||
skippedUpdates.add(
|
||||
manga to context.getString(
|
||||
R.string.skipped_reason_not_started,
|
||||
),
|
||||
)
|
||||
|
||||
ENTRY_OUTSIDE_RELEASE_PERIOD in restrictions && manga.nextUpdate > fetchWindow.second ->
|
||||
skippedUpdates.add(
|
||||
manga to context.getString(
|
||||
R.string.skipped_reason_not_in_release_period,
|
||||
),
|
||||
)
|
||||
|
||||
else -> {
|
||||
try {
|
||||
val newChapters = updateManga(manga, fetchWindow)
|
||||
.sortedByDescending { it.sourceOrder }
|
||||
|
||||
if (newChapters.isNotEmpty()) {
|
||||
val categoryIds = getCategories.await(manga.id).map { it.id }
|
||||
if (manga.shouldDownloadNewChapters(
|
||||
categoryIds,
|
||||
downloadPreferences,
|
||||
)
|
||||
) {
|
||||
downloadChapters(manga, newChapters)
|
||||
hasDownloads.set(true)
|
||||
}
|
||||
|
||||
libraryPreferences.newMangaUpdatesCount().getAndSet { it + newChapters.size }
|
||||
|
||||
// Convert to the manga that contains new chapters
|
||||
newUpdates.add(
|
||||
manga to newChapters.toTypedArray(),
|
||||
)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
val errorMessage = when (e) {
|
||||
is NoChaptersException -> context.getString(
|
||||
R.string.no_chapters_error,
|
||||
)
|
||||
// failedUpdates will already have the source, don't need to copy it into the message
|
||||
is SourceNotInstalledException -> context.getString(
|
||||
R.string.loader_not_implemented_error,
|
||||
)
|
||||
else -> e.message
|
||||
}
|
||||
failedUpdates.add(manga to errorMessage)
|
||||
if (newChapters.isNotEmpty()) {
|
||||
val categoryIds = getCategories.await(manga.id).map { it.id }
|
||||
if (manga.shouldDownloadNewChapters(categoryIds, downloadPreferences)) {
|
||||
downloadChapters(manga, newChapters)
|
||||
hasDownloads.set(true)
|
||||
}
|
||||
libraryPreferences.newMangaUpdatesCount().getAndSet { it + newChapters.size }
|
||||
|
||||
// Convert to the manga that contains new chapters
|
||||
newUpdates.add(manga to newChapters.toTypedArray())
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
val errorMessage = when (e) {
|
||||
is NoChaptersException -> context.getString(R.string.no_chapters_error)
|
||||
// failedUpdates will already have the source, don't need to copy it into the message
|
||||
is SourceNotInstalledException -> context.getString(R.string.loader_not_implemented_error)
|
||||
else -> e.message
|
||||
}
|
||||
failedUpdates.add(manga to errorMessage)
|
||||
}
|
||||
|
||||
if (libraryPreferences.autoUpdateTrackers().get()) {
|
||||
|
@ -339,16 +331,6 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
errorFile.getUriCompat(context),
|
||||
)
|
||||
}
|
||||
if (skippedUpdates.isNotEmpty()) {
|
||||
// TODO: surface skipped reasons to user
|
||||
logcat {
|
||||
skippedUpdates
|
||||
.groupBy { it.second }
|
||||
.map { (reason, entries) -> "$reason: [${entries.map { it.first.title }.sorted().joinToString()}]" }
|
||||
.joinToString()
|
||||
}
|
||||
notifier.showUpdateSkippedNotification(skippedUpdates.size)
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
||||
|
@ -466,29 +448,27 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
completed: AtomicInteger,
|
||||
manga: Manga,
|
||||
block: suspend () -> Unit,
|
||||
) {
|
||||
coroutineScope {
|
||||
ensureActive()
|
||||
) = coroutineScope {
|
||||
ensureActive()
|
||||
|
||||
updatingManga.add(manga)
|
||||
notifier.showProgressNotification(
|
||||
updatingManga,
|
||||
completed.get(),
|
||||
mangaToUpdate.size,
|
||||
)
|
||||
updatingManga.add(manga)
|
||||
notifier.showProgressNotification(
|
||||
updatingManga,
|
||||
completed.get(),
|
||||
mangaToUpdate.size,
|
||||
)
|
||||
|
||||
block()
|
||||
block()
|
||||
|
||||
ensureActive()
|
||||
ensureActive()
|
||||
|
||||
updatingManga.remove(manga)
|
||||
completed.getAndIncrement()
|
||||
notifier.showProgressNotification(
|
||||
updatingManga,
|
||||
completed.get(),
|
||||
mangaToUpdate.size,
|
||||
)
|
||||
}
|
||||
updatingManga.remove(manga)
|
||||
completed.getAndIncrement()
|
||||
notifier.showProgressNotification(
|
||||
updatingManga,
|
||||
completed.get(),
|
||||
mangaToUpdate.size,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -30,10 +30,14 @@ import tachiyomi.core.util.lang.launchUI
|
|||
import tachiyomi.domain.entries.manga.model.Manga
|
||||
import tachiyomi.domain.items.chapter.model.Chapter
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.NumberFormat
|
||||
|
||||
class MangaLibraryUpdateNotifier(private val context: Context) {
|
||||
|
||||
private val preferences: SecurityPreferences by injectLazy()
|
||||
private val percentFormatter = NumberFormat.getPercentInstance().apply {
|
||||
maximumFractionDigits = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Pending intent of action that cancels the library update
|
||||
|
@ -82,7 +86,7 @@ class MangaLibraryUpdateNotifier(private val context: Context) {
|
|||
} else {
|
||||
val updatingText = manga.joinToString("\n") { it.title.chop(40) }
|
||||
progressNotificationBuilder
|
||||
.setContentTitle(context.getString(R.string.notification_updating, current, total))
|
||||
.setContentTitle(context.getString(R.string.notification_updating_progress, percentFormatter.format(current.toFloat() / total)))
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(updatingText))
|
||||
}
|
||||
|
||||
|
|
|
@ -173,19 +173,12 @@ sealed class Image(
|
|||
}
|
||||
|
||||
sealed interface Location {
|
||||
data class Pictures private constructor(val relativePath: String) : Location {
|
||||
companion object {
|
||||
fun create(relativePath: String = ""): Pictures {
|
||||
return Pictures(relativePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
data class Pictures(val relativePath: String) : Location
|
||||
|
||||
data object Cache : Location
|
||||
|
||||
fun directory(context: Context): File {
|
||||
return when (this) {
|
||||
Cache -> context.cacheImageDir
|
||||
is Pictures -> {
|
||||
val file = File(
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
|
||||
|
@ -199,6 +192,7 @@ sealed interface Location {
|
|||
}
|
||||
file
|
||||
}
|
||||
Cache -> context.cacheImageDir
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import android.content.Intent
|
|||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import eu.kanade.tachiyomi.util.lang.use
|
||||
import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat
|
||||
|
@ -104,7 +105,7 @@ class PackageInstallerInstallerAnime(private val service: Service) : InstallerAn
|
|||
}
|
||||
|
||||
init {
|
||||
service.registerReceiver(packageActionReceiver, IntentFilter(INSTALL_ACTION))
|
||||
ContextCompat.registerReceiver(service, packageActionReceiver, IntentFilter(INSTALL_ACTION), ContextCompat.RECEIVER_EXPORTED)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -272,7 +272,7 @@ internal class AnimeExtensionInstaller(private val context: Context) {
|
|||
isRegistered = true
|
||||
|
||||
val filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
|
||||
context.registerReceiver(this, filter)
|
||||
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -8,6 +8,7 @@ import android.content.Intent
|
|||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import eu.kanade.tachiyomi.util.lang.use
|
||||
import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat
|
||||
|
@ -104,7 +105,7 @@ class PackageInstallerInstallerManga(private val service: Service) : InstallerMa
|
|||
}
|
||||
|
||||
init {
|
||||
service.registerReceiver(packageActionReceiver, IntentFilter(INSTALL_ACTION))
|
||||
ContextCompat.registerReceiver(service, packageActionReceiver, IntentFilter(INSTALL_ACTION), ContextCompat.RECEIVER_EXPORTED)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -272,7 +272,7 @@ internal class MangaExtensionInstaller(private val context: Context) {
|
|||
isRegistered = true
|
||||
|
||||
val filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
|
||||
context.registerReceiver(this, filter)
|
||||
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -11,6 +11,8 @@ import uy.kohesive.injekt.api.get
|
|||
|
||||
fun AnimeSource.icon(): Drawable? = Injekt.get<AnimeExtensionManager>().getAppIconForSource(this.id)
|
||||
|
||||
fun AnimeSource.getPreferenceKey(): String = "source_$id"
|
||||
|
||||
fun AnimeSource.toStubSource(): StubAnimeSource = StubAnimeSource(id = id, lang = lang, name = name)
|
||||
|
||||
fun AnimeSource.getNameForAnimeInfo(): String {
|
||||
|
|
|
@ -11,6 +11,8 @@ import uy.kohesive.injekt.api.get
|
|||
|
||||
fun MangaSource.icon(): Drawable? = Injekt.get<MangaExtensionManager>().getAppIconForSource(this.id)
|
||||
|
||||
fun MangaSource.getPreferenceKey(): String = "source_$id"
|
||||
|
||||
fun MangaSource.toStubSource(): StubMangaSource = StubMangaSource(id = id, lang = lang, name = name)
|
||||
|
||||
fun MangaSource.getNameForMangaInfo(): String {
|
||||
|
|
|
@ -88,6 +88,15 @@ class AnimeCategoryScreenModel(
|
|||
}
|
||||
}
|
||||
|
||||
fun sortAlphabetically() {
|
||||
coroutineScope.launch {
|
||||
when (reorderCategory.sortAlphabetically()) {
|
||||
is ReorderAnimeCategory.Result.InternalError -> _events.send(AnimeCategoryEvent.InternalError)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun moveUp(category: Category) {
|
||||
coroutineScope.launch {
|
||||
when (reorderCategory.moveUp(category)) {
|
||||
|
@ -142,6 +151,7 @@ class AnimeCategoryScreenModel(
|
|||
|
||||
sealed interface AnimeCategoryDialog {
|
||||
data object Create : AnimeCategoryDialog
|
||||
data object SortAlphabetically : AnimeCategoryDialog
|
||||
data class Rename(val category: Category) : AnimeCategoryDialog
|
||||
data class Delete(val category: Category) : AnimeCategoryDialog
|
||||
}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
package eu.kanade.tachiyomi.ui.category.anime
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.SortByAlpha
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
|
@ -11,6 +14,8 @@ import eu.kanade.presentation.category.AnimeCategoryScreen
|
|||
import eu.kanade.presentation.category.components.CategoryCreateDialog
|
||||
import eu.kanade.presentation.category.components.CategoryDeleteDialog
|
||||
import eu.kanade.presentation.category.components.CategoryRenameDialog
|
||||
import eu.kanade.presentation.category.components.CategorySortAlphabeticallyDialog
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.TabContent
|
||||
import eu.kanade.tachiyomi.R
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
|
@ -25,6 +30,14 @@ fun Screen.animeCategoryTab(): TabContent {
|
|||
return TabContent(
|
||||
titleRes = R.string.label_anime,
|
||||
searchEnabled = false,
|
||||
actions =
|
||||
listOf(
|
||||
AppBar.Action(
|
||||
title = stringResource(R.string.action_sort),
|
||||
icon = Icons.Outlined.SortByAlpha,
|
||||
onClick = { screenModel.showDialog(AnimeCategoryDialog.SortAlphabetically) },
|
||||
),
|
||||
),
|
||||
content = { contentPadding, _ ->
|
||||
if (state is AnimeCategoryScreenState.Loading) {
|
||||
LoadingScreen()
|
||||
|
@ -66,6 +79,12 @@ fun Screen.animeCategoryTab(): TabContent {
|
|||
category = dialog.category,
|
||||
)
|
||||
}
|
||||
is AnimeCategoryDialog.SortAlphabetically -> {
|
||||
CategorySortAlphabeticallyDialog(
|
||||
onDismissRequest = screenModel::dismissDialog,
|
||||
onSort = { screenModel.sortAlphabetically() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -88,6 +88,15 @@ class MangaCategoryScreenModel(
|
|||
}
|
||||
}
|
||||
|
||||
fun sortAlphabetically() {
|
||||
coroutineScope.launch {
|
||||
when (reorderCategory.sortAlphabetically()) {
|
||||
is ReorderMangaCategory.Result.InternalError -> _events.send(MangaCategoryEvent.InternalError)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun moveUp(category: Category) {
|
||||
coroutineScope.launch {
|
||||
when (reorderCategory.moveUp(category)) {
|
||||
|
@ -142,6 +151,7 @@ class MangaCategoryScreenModel(
|
|||
|
||||
sealed interface MangaCategoryDialog {
|
||||
data object Create : MangaCategoryDialog
|
||||
data object SortAlphabetically : MangaCategoryDialog
|
||||
data class Rename(val category: Category) : MangaCategoryDialog
|
||||
data class Delete(val category: Category) : MangaCategoryDialog
|
||||
}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
package eu.kanade.tachiyomi.ui.category.manga
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.SortByAlpha
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
|
@ -11,6 +14,8 @@ import eu.kanade.presentation.category.MangaCategoryScreen
|
|||
import eu.kanade.presentation.category.components.CategoryCreateDialog
|
||||
import eu.kanade.presentation.category.components.CategoryDeleteDialog
|
||||
import eu.kanade.presentation.category.components.CategoryRenameDialog
|
||||
import eu.kanade.presentation.category.components.CategorySortAlphabeticallyDialog
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.TabContent
|
||||
import eu.kanade.tachiyomi.R
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
|
@ -25,6 +30,13 @@ fun Screen.mangaCategoryTab(): TabContent {
|
|||
return TabContent(
|
||||
titleRes = R.string.label_manga,
|
||||
searchEnabled = false,
|
||||
actions = listOf(
|
||||
AppBar.Action(
|
||||
title = stringResource(R.string.action_sort),
|
||||
icon = Icons.Outlined.SortByAlpha,
|
||||
onClick = { screenModel.showDialog(MangaCategoryDialog.SortAlphabetically) },
|
||||
),
|
||||
),
|
||||
content = { contentPadding, _ ->
|
||||
|
||||
if (state is MangaCategoryScreenState.Loading) {
|
||||
|
@ -67,6 +79,12 @@ fun Screen.mangaCategoryTab(): TabContent {
|
|||
category = dialog.category,
|
||||
)
|
||||
}
|
||||
is MangaCategoryDialog.SortAlphabetically -> {
|
||||
CategorySortAlphabeticallyDialog(
|
||||
onDismissRequest = screenModel::dismissDialog,
|
||||
onSort = { screenModel.sortAlphabetically() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -102,8 +102,8 @@ class AnimeCoverScreenModel(
|
|||
imageSaver.save(
|
||||
Image.Cover(
|
||||
bitmap = bitmap,
|
||||
name = anime.title,
|
||||
location = if (temp) Location.Cache else Location.Pictures.create(),
|
||||
name = "cover",
|
||||
location = if (temp) Location.Cache else Location.Pictures(anime.title),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import eu.kanade.domain.entries.anime.model.downloadedFilter
|
|||
import eu.kanade.domain.entries.anime.model.toSAnime
|
||||
import eu.kanade.domain.items.episode.interactor.SetSeenStatus
|
||||
import eu.kanade.domain.items.episode.interactor.SyncEpisodesWithSource
|
||||
import eu.kanade.domain.track.anime.interactor.AddAnimeTracks
|
||||
import eu.kanade.domain.track.service.TrackPreferences
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.presentation.entries.DownloadAction
|
||||
|
@ -103,6 +104,7 @@ class AnimeScreenModel(
|
|||
private val syncEpisodesWithSource: SyncEpisodesWithSource = Injekt.get(),
|
||||
private val getCategories: GetAnimeCategories = Injekt.get(),
|
||||
private val getTracks: GetAnimeTracks = Injekt.get(),
|
||||
private val addTracks: AddAnimeTracks = Injekt.get(),
|
||||
private val setAnimeCategories: SetAnimeCategories = Injekt.get(),
|
||||
private val animeRepository: AnimeRepository = Injekt.get(),
|
||||
internal val setAnimeViewerFlags: SetAnimeViewerFlags = Injekt.get(),
|
||||
|
@ -333,24 +335,7 @@ class AnimeScreenModel(
|
|||
}
|
||||
|
||||
// Finally match with enhanced tracking when available
|
||||
val source = state.source
|
||||
state.trackItems
|
||||
.map { it.tracker }
|
||||
.filterIsInstance<EnhancedAnimeTracker>()
|
||||
.filter { it.accept(source) }
|
||||
.forEach { service ->
|
||||
launchIO {
|
||||
try {
|
||||
service.match(anime)?.let { track ->
|
||||
(service as AnimeTracker).register(track, animeId)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.WARN, e) {
|
||||
"Could not match anime: ${anime.title} with service $service"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
addTracks.bindEnhancedTracks(anime, state.source)
|
||||
if (autoOpenTrack) {
|
||||
showTrackDialog()
|
||||
}
|
||||
|
|
|
@ -102,8 +102,8 @@ class MangaCoverScreenModel(
|
|||
imageSaver.save(
|
||||
Image.Cover(
|
||||
bitmap = bitmap,
|
||||
name = manga.title,
|
||||
location = if (temp) Location.Cache else Location.Pictures.create(),
|
||||
name = "cover",
|
||||
location = if (temp) Location.Cache else Location.Pictures(manga.title),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import eu.kanade.domain.entries.manga.model.downloadedFilter
|
|||
import eu.kanade.domain.entries.manga.model.toSManga
|
||||
import eu.kanade.domain.items.chapter.interactor.SetReadStatus
|
||||
import eu.kanade.domain.items.chapter.interactor.SyncChaptersWithSource
|
||||
import eu.kanade.domain.track.manga.interactor.AddMangaTracks
|
||||
import eu.kanade.domain.track.service.TrackPreferences
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.presentation.entries.DownloadAction
|
||||
|
@ -99,6 +100,7 @@ class MangaScreenModel(
|
|||
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(),
|
||||
private val getCategories: GetMangaCategories = Injekt.get(),
|
||||
private val getTracks: GetMangaTracks = Injekt.get(),
|
||||
private val addTracks: AddMangaTracks = Injekt.get(),
|
||||
private val setMangaCategories: SetMangaCategories = Injekt.get(),
|
||||
private val mangaRepository: MangaRepository = Injekt.get(),
|
||||
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
||||
|
@ -329,24 +331,7 @@ class MangaScreenModel(
|
|||
}
|
||||
|
||||
// Finally match with enhanced tracking when available
|
||||
val source = state.source
|
||||
state.trackItems
|
||||
.map { it.tracker }
|
||||
.filterIsInstance<EnhancedMangaTracker>()
|
||||
.filter { it.accept(source) }
|
||||
.forEach { service ->
|
||||
launchIO {
|
||||
try {
|
||||
service.match(manga)?.let { track ->
|
||||
(service as MangaTracker).register(track, mangaId)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.WARN, e) {
|
||||
"Could not match manga: ${manga.title} with service $service"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
addTracks.bindEnhancedTracks(manga, state.source)
|
||||
if (autoOpenTrack) {
|
||||
showTrackDialog()
|
||||
}
|
||||
|
|
|
@ -170,6 +170,7 @@ class MainActivity : BaseActivity() {
|
|||
}
|
||||
|
||||
// Draw edge-to-edge
|
||||
// TODO: replace with ComponentActivity#enableEdgeToEdge
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
setComposeContent {
|
||||
|
|
|
@ -498,7 +498,7 @@ class PlayerViewModel @JvmOverloads constructor(
|
|||
image = Image.Page(
|
||||
inputStream = imageStream,
|
||||
name = filename,
|
||||
location = Location.Pictures.create(relativePath),
|
||||
location = Location.Pictures(relativePath),
|
||||
),
|
||||
)
|
||||
notifier.onComplete(uri)
|
||||
|
|
|
@ -27,7 +27,6 @@ import eu.kanade.tachiyomi.ui.reader.loader.DownloadPageLoader
|
|||
import eu.kanade.tachiyomi.ui.reader.model.InsertPage
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.ui.reader.model.StencilPage
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||
|
@ -425,8 +424,8 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||
* [page]'s chapter is different from the currently active.
|
||||
*/
|
||||
fun onPageSelected(page: ReaderPage) {
|
||||
// InsertPage and StencilPage doesn't change page progress
|
||||
if (page is InsertPage || page is StencilPage) {
|
||||
// InsertPage doesn't change page progress
|
||||
if (page is InsertPage) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -766,23 +765,14 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||
|
||||
val filename = generateFilename(manga, page)
|
||||
|
||||
// Pictures directory.
|
||||
val relativePath = if (readerPreferences.folderPerManga().get()) {
|
||||
DiskUtil.buildValidFilename(
|
||||
manga.title,
|
||||
)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
// Copy file in background.
|
||||
// Copy file in background
|
||||
viewModelScope.launchNonCancellable {
|
||||
try {
|
||||
val uri = imageSaver.save(
|
||||
image = Image.Page(
|
||||
inputStream = page.stream!!,
|
||||
name = filename,
|
||||
location = Location.Pictures.create(relativePath),
|
||||
location = Location.Pictures(DiskUtil.buildValidFilename(manga.title)),
|
||||
),
|
||||
)
|
||||
withUIContext {
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
package eu.kanade.tachiyomi.ui.reader.model
|
||||
|
||||
import java.io.InputStream
|
||||
|
||||
class StencilPage(
|
||||
parent: ReaderPage,
|
||||
stencilStream: () -> InputStream,
|
||||
) : ReaderPage(parent.index, parent.url, parent.imageUrl) {
|
||||
|
||||
override var chapter: ReaderChapter = parent.chapter
|
||||
|
||||
init {
|
||||
status = State.READY
|
||||
stream = stencilStream
|
||||
}
|
||||
}
|
|
@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.reader.setting
|
|||
|
||||
import androidx.annotation.StringRes
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
import tachiyomi.core.preference.getEnum
|
||||
|
||||
|
@ -39,12 +38,6 @@ class ReaderPreferences(
|
|||
OrientationType.FREE.flagValue,
|
||||
)
|
||||
|
||||
// TODO: Enable in release build when the feature is stable
|
||||
fun longStripSplitWebtoon() = preferenceStore.getBoolean(
|
||||
"pref_long_strip_split_webtoon",
|
||||
!isReleaseBuildType,
|
||||
)
|
||||
|
||||
fun webtoonDoubleTapZoomEnabled() = preferenceStore.getBoolean(
|
||||
"pref_enable_double_tap_zoom_webtoon",
|
||||
true,
|
||||
|
@ -81,8 +74,6 @@ class ReaderPreferences(
|
|||
ReaderHideThreshold.LOW,
|
||||
)
|
||||
|
||||
fun folderPerManga() = preferenceStore.getBoolean("create_folder_per_manga", false)
|
||||
|
||||
fun skipRead() = preferenceStore.getBoolean("skip_read", false)
|
||||
|
||||
fun skipFiltered() = preferenceStore.getBoolean("skip_filtered", true)
|
||||
|
|
|
@ -7,12 +7,10 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.ui.reader.model.StencilPage
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.calculateChapterGap
|
||||
import eu.kanade.tachiyomi.util.system.createReaderThemeContext
|
||||
import tachiyomi.core.util.system.logcat
|
||||
|
||||
/**
|
||||
* RecyclerView Adapter used by this [viewer] to where [ViewerChapters] updates are posted.
|
||||
|
@ -27,27 +25,6 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter<RecyclerV
|
|||
|
||||
var currentChapter: ReaderChapter? = null
|
||||
|
||||
fun onLongStripSplit(currentStrip: Any?, newStrips: List<StencilPage>) {
|
||||
if (newStrips.isEmpty()) return
|
||||
if (currentStrip is StencilPage) return
|
||||
|
||||
val placeAtIndex = items.indexOf(currentStrip) + 1
|
||||
// Stop constantly adding split images
|
||||
if (items.getOrNull(placeAtIndex) is StencilPage) return
|
||||
|
||||
val updatedItems = items.toMutableList()
|
||||
updatedItems.addAll(placeAtIndex, newStrips)
|
||||
updateItems(updatedItems)
|
||||
logcat { "New adapter item count is $itemCount" }
|
||||
}
|
||||
|
||||
fun cleanupSplitStrips() {
|
||||
if (items.any { it is StencilPage }) {
|
||||
val updatedItems = items.filterNot { it is StencilPage }
|
||||
updateItems(updatedItems)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Context that has been wrapped to use the correct theme values based on the
|
||||
* current app theme and reader background color
|
||||
|
|
|
@ -32,11 +32,6 @@ class WebtoonConfig(
|
|||
var sidePadding = 0
|
||||
private set
|
||||
|
||||
var longStripSplit = false
|
||||
private set
|
||||
|
||||
var longStripSplitChangedListener: ((Boolean) -> Unit)? = null
|
||||
|
||||
var doubleTapZoom = true
|
||||
private set
|
||||
|
||||
|
@ -67,15 +62,6 @@ class WebtoonConfig(
|
|||
readerPreferences.dualPageInvertWebtoon()
|
||||
.register({ dualPageInvert = it }, { imagePropertyChangedListener?.invoke() })
|
||||
|
||||
readerPreferences.longStripSplitWebtoon()
|
||||
.register(
|
||||
{ longStripSplit = it },
|
||||
{
|
||||
imagePropertyChangedListener?.invoke()
|
||||
longStripSplitChangedListener?.invoke(it)
|
||||
},
|
||||
)
|
||||
|
||||
readerPreferences.webtoonDoubleTapZoomEnabled()
|
||||
.register(
|
||||
{ doubleTapZoom = it },
|
||||
|
|
|
@ -13,7 +13,6 @@ import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
|||
import eu.kanade.tachiyomi.databinding.ReaderErrorBinding
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.ui.reader.model.StencilPage
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
|
||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
||||
|
@ -24,12 +23,10 @@ import kotlinx.coroutines.flow.collectLatest
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.lang.launchIO
|
||||
import tachiyomi.core.util.lang.withIOContext
|
||||
import tachiyomi.core.util.lang.withUIContext
|
||||
import tachiyomi.core.util.system.ImageUtil
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.InputStream
|
||||
|
||||
|
@ -221,40 +218,9 @@ class WebtoonPageHolder(
|
|||
}
|
||||
}
|
||||
|
||||
if (viewer.config.longStripSplit) {
|
||||
if (page is StencilPage) {
|
||||
return imageStream
|
||||
}
|
||||
val isStripSplitNeeded = ImageUtil.isStripSplitNeeded(imageStream)
|
||||
if (isStripSplitNeeded) {
|
||||
return onStripSplit(imageStream)
|
||||
}
|
||||
}
|
||||
|
||||
return imageStream
|
||||
}
|
||||
|
||||
private fun onStripSplit(imageStream: BufferedInputStream): InputStream {
|
||||
try {
|
||||
// If we have reached this point [page] and its stream shouldn't be null
|
||||
val page = page!!
|
||||
val stream = page.stream!!
|
||||
val splitData = ImageUtil.getSplitDataForStream(imageStream).toMutableList()
|
||||
val currentSplitData = splitData.removeFirst()
|
||||
val newPages = splitData.map {
|
||||
StencilPage(page) { ImageUtil.splitStrip(it, stream) }
|
||||
}
|
||||
return ImageUtil.splitStrip(currentSplitData) { imageStream }
|
||||
.also {
|
||||
// Running [onLongStripSplit] first results in issues with splitting
|
||||
viewer.onLongStripSplit(page, newPages)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e) { "Failed to split image" }
|
||||
return imageStream
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the page has an error.
|
||||
*/
|
||||
|
|
|
@ -15,7 +15,6 @@ import eu.kanade.tachiyomi.data.download.manga.MangaDownloadManager
|
|||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.ui.reader.model.StencilPage
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.Viewer
|
||||
|
@ -151,12 +150,6 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
|
|||
activity.binding.navigationOverlay.setNavigation(config.navigator, showOnStart)
|
||||
}
|
||||
|
||||
config.longStripSplitChangedListener = { enabled ->
|
||||
if (!enabled) {
|
||||
cleanupSplitStrips()
|
||||
}
|
||||
}
|
||||
|
||||
frame.layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
|
||||
frame.addView(recycler)
|
||||
}
|
||||
|
@ -205,11 +198,6 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
|
|||
logcat { "onPageSelected: ${page.number}/${pages.size}" }
|
||||
activity.onPageSelected(page)
|
||||
|
||||
// Skip preload on StencilPage
|
||||
if (page is StencilPage) {
|
||||
return
|
||||
}
|
||||
|
||||
// Preload next chapter once we're within the last 5 pages of the current chapter
|
||||
val inPreloadRange = pages.size - page.number < 5
|
||||
if (inPreloadRange && allowPreload && page.chapter == adapter.currentChapter) {
|
||||
|
@ -359,15 +347,4 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
|
|||
min(position + 3, adapter.itemCount - 1),
|
||||
)
|
||||
}
|
||||
|
||||
fun onLongStripSplit(currentStrip: Any?, newStrips: List<StencilPage>) {
|
||||
activity.runOnUiThread {
|
||||
// Need to insert on UI thread else images will go blank
|
||||
adapter.onLongStripSplit(currentStrip, newStrips)
|
||||
}
|
||||
}
|
||||
|
||||
private fun cleanupSplitStrips() {
|
||||
adapter.cleanupSplitStrips()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.util
|
|||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.data.backup.BackupManager
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreator
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
|
||||
|
@ -29,7 +29,7 @@ object BackupUtil {
|
|||
* Decode a potentially-gzipped backup.
|
||||
*/
|
||||
fun decodeBackup(context: Context, uri: Uri): Backup {
|
||||
val backupManager = BackupManager(context)
|
||||
val backupCreator = BackupCreator(context)
|
||||
|
||||
val backupStringSource = context.contentResolver.openInputStream(uri)!!.source().buffer()
|
||||
|
||||
|
@ -43,9 +43,9 @@ object BackupUtil {
|
|||
}.use { it.readByteArray() }
|
||||
|
||||
return try {
|
||||
backupManager.parser.decodeFromByteArray(BackupSerializer, backupString)
|
||||
backupCreator.parser.decodeFromByteArray(BackupSerializer, backupString)
|
||||
} catch (e: SerializationException) {
|
||||
val fullBackup = backupManager.parser.decodeFromByteArray(
|
||||
val fullBackup = backupCreator.parser.decodeFromByteArray(
|
||||
FullBackupSerializer,
|
||||
backupString,
|
||||
)
|
||||
|
|
|
@ -283,48 +283,6 @@ object ImageUtil {
|
|||
index + 1,
|
||||
)}.jpg"
|
||||
|
||||
/**
|
||||
* Check whether the image is a long Strip that needs splitting
|
||||
* @return true if the image is not animated and it's height is greater than image width and screen height
|
||||
*/
|
||||
fun isStripSplitNeeded(imageStream: BufferedInputStream): Boolean {
|
||||
if (isAnimatedAndSupported(imageStream)) return false
|
||||
|
||||
val options = extractImageOptions(imageStream)
|
||||
val imageHeightIsBiggerThanWidth = options.outHeight > options.outWidth
|
||||
val imageHeightBiggerThanScreenHeight = options.outHeight > optimalImageHeight
|
||||
return imageHeightIsBiggerThanWidth && imageHeightBiggerThanScreenHeight
|
||||
}
|
||||
|
||||
/**
|
||||
* Split the imageStream according to the provided splitData
|
||||
*/
|
||||
fun splitStrip(splitData: SplitData, streamFn: () -> InputStream): InputStream {
|
||||
val bitmapRegionDecoder = getBitmapRegionDecoder(streamFn())
|
||||
?: throw Exception("Failed to create new instance of BitmapRegionDecoder")
|
||||
|
||||
logcat {
|
||||
"WebtoonSplit #${splitData.index} with topOffset=${splitData.topOffset} " +
|
||||
"splitHeight=${splitData.splitHeight} bottomOffset=${splitData.bottomOffset}"
|
||||
}
|
||||
|
||||
try {
|
||||
val region = Rect(0, splitData.topOffset, splitData.splitWidth, splitData.bottomOffset)
|
||||
val splitBitmap = bitmapRegionDecoder.decodeRegion(region, null)
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
splitBitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
|
||||
return ByteArrayInputStream(outputStream.toByteArray())
|
||||
} catch (e: Throwable) {
|
||||
throw e
|
||||
} finally {
|
||||
bitmapRegionDecoder.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
fun getSplitDataForStream(imageStream: InputStream): List<SplitData> {
|
||||
return extractImageOptions(imageStream).splitData
|
||||
}
|
||||
|
||||
private val BitmapFactory.Options.splitData
|
||||
get(): List<SplitData> {
|
||||
val imageHeight = outHeight
|
||||
|
|
|
@ -57,6 +57,27 @@ class ReorderAnimeCategory(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun sortAlphabetically() = withNonCancellableContext {
|
||||
mutex.withLock {
|
||||
val updates = categoryRepository.getAllAnimeCategories()
|
||||
.sortedBy { category -> category.name }
|
||||
.mapIndexed { index, category ->
|
||||
CategoryUpdate(
|
||||
id = category.id,
|
||||
order = index.toLong(),
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
categoryRepository.updatePartialAnimeCategories(updates)
|
||||
Result.Success
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
Result.InternalError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface Result {
|
||||
data object Success : Result
|
||||
data object Unchanged : Result
|
||||
|
|
|
@ -57,6 +57,27 @@ class ReorderMangaCategory(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun sortAlphabetically() = withNonCancellableContext {
|
||||
mutex.withLock {
|
||||
val updates = categoryRepository.getAllMangaCategories()
|
||||
.sortedBy { category -> category.name }
|
||||
.mapIndexed { index, category ->
|
||||
CategoryUpdate(
|
||||
id = category.id,
|
||||
order = index.toLong(),
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
categoryRepository.updatePartialMangaCategories(updates)
|
||||
Result.Success
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
Result.InternalError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface Result {
|
||||
data object Success : Result
|
||||
data object Unchanged : Result
|
||||
|
|
|
@ -24,5 +24,4 @@ kotlin.mpp.androidSourceSetLayoutVersion=2
|
|||
|
||||
android.useAndroidX=true
|
||||
android.defaults.buildfeatures.buildconfig=true
|
||||
android.nonTransitiveRClass=false
|
||||
#android.nonFinalResIds=false
|
||||
android.nonTransitiveRClass=false
|
|
@ -1,5 +1,5 @@
|
|||
[versions]
|
||||
agp_version = "8.1.1"
|
||||
agp_version = "8.1.2"
|
||||
lifecycle_version = "2.6.2"
|
||||
paging_version = "3.2.1"
|
||||
|
||||
|
@ -28,7 +28,7 @@ guava = "com.google.guava:guava:32.1.2-android"
|
|||
paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "paging_version" }
|
||||
paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging_version" }
|
||||
|
||||
benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.2.0-rc01"
|
||||
benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.2.0-rc02"
|
||||
test-ext = "androidx.test.ext:junit-ktx:1.2.0-alpha01"
|
||||
test-espresso-core = "androidx.test.espresso:espresso-core:3.6.0-alpha01"
|
||||
test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0-alpha04"
|
||||
|
|
|
@ -4,7 +4,7 @@ compose-bom = "2023.09.00-alpha02"
|
|||
accompanist = "0.33.1-alpha"
|
||||
|
||||
[libraries]
|
||||
activity = "androidx.activity:activity-compose:1.7.2"
|
||||
activity = "androidx.activity:activity-compose:1.8.0"
|
||||
bom = { group = "dev.chrisbanes.compose", name = "compose-bom", version.ref = "compose-bom" }
|
||||
foundation = { module = "androidx.compose.foundation:foundation" }
|
||||
animation = { module = "androidx.compose.animation:animation" }
|
||||
|
|
|
@ -18,7 +18,7 @@ flowreactivenetwork = "ru.beryukhov:flowreactivenetwork:1.0.4"
|
|||
okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp_version" }
|
||||
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp_version" }
|
||||
okhttp-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "okhttp_version" }
|
||||
okio = "com.squareup.okio:okio:3.5.0"
|
||||
okio = "com.squareup.okio:okio:3.6.0"
|
||||
|
||||
conscrypt-android = "org.conscrypt:conscrypt-android:2.5.2"
|
||||
|
||||
|
@ -51,13 +51,12 @@ natural-comparator = "com.github.gpanther:java-nat-sort:natural-comparator-1.1"
|
|||
richtext-commonmark = { module = "com.halilibo.compose-richtext:richtext-commonmark", version.ref = "richtext" }
|
||||
richtext-m3 = { module = "com.halilibo.compose-richtext:richtext-ui-material3", version.ref = "richtext" }
|
||||
|
||||
material = "com.google.android.material:material:1.9.0"
|
||||
material = "com.google.android.material:material:1.10.0"
|
||||
flexible-adapter-core = "com.github.arkon.FlexibleAdapter:flexible-adapter:c8013533"
|
||||
photoview = "com.github.chrisbanes:PhotoView:2.3.0"
|
||||
directionalviewpager = "com.github.tachiyomiorg:DirectionalViewPager:1.0.0"
|
||||
insetter = "dev.chrisbanes.insetter:insetter:0.6.1"
|
||||
compose-materialmotion = "io.github.fornewid:material-motion-compose-core:1.1.0"
|
||||
compose-simpleicons = "br.com.devsrsouza.compose.icons.android:simple-icons:1.0.0"
|
||||
|
||||
swipe = "me.saket.swipe:swipe:1.2.0"
|
||||
|
||||
|
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,6 +1,6 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
|
@ -92,6 +92,8 @@
|
|||
<string name="action_move_category">Set categories</string>
|
||||
<string name="delete_category_confirmation">Do you wish to delete the category \"%s\"?</string>
|
||||
<string name="delete_category">Delete category</string>
|
||||
<string name="action_sort_category">Sort categories</string>
|
||||
<string name="sort_category_confirmation">Would you like to sort the categories alphabetically?</string>
|
||||
<string name="action_edit_cover">Edit cover</string>
|
||||
<string name="action_view_chapters">View chapters</string>
|
||||
<string name="action_pause">Pause</string>
|
||||
|
@ -328,7 +330,6 @@
|
|||
<string name="pref_dual_page_invert_summary">If the placement of the split wide pages don\'t match reading direction</string>
|
||||
<string name="pref_page_rotate">Rotate wide pages to fit</string>
|
||||
<string name="pref_page_rotate_invert">Flip orientation of rotated wide pages</string>
|
||||
<string name="pref_long_strip_split">Split tall images (BETA)</string>
|
||||
<string name="pref_double_tap_zoom">Double tap to zoom</string>
|
||||
<string name="pref_cutout_short">Show content in cutout area</string>
|
||||
<string name="pref_page_transitions">Animate page transitions</string>
|
||||
|
@ -364,8 +365,6 @@
|
|||
<string name="tapping_inverted_both">Both</string>
|
||||
<string name="pref_reader_actions">Actions</string>
|
||||
<string name="pref_read_with_long_tap">Show on actions long tap</string>
|
||||
<string name="pref_create_folder_per_manga">Save pages into separate folders</string>
|
||||
<string name="pref_create_folder_per_manga_summary">Creates folders according to entries\' title</string>
|
||||
<string name="pref_reader_theme">Background color</string>
|
||||
<string name="white_background">White</string>
|
||||
<string name="gray_background">Gray</string>
|
||||
|
@ -819,7 +818,7 @@
|
|||
|
||||
<!-- Library update service notifications -->
|
||||
<string name="notification_check_updates">Checking for new chapters</string>
|
||||
<string name="notification_updating">Updating library… (%1$d/%2$d)</string>
|
||||
<string name="notification_updating_progress">Updating library… (%s)</string>
|
||||
<string name="notification_size_warning">Large updates harm sources and may lead to slower updates and also increased battery usage. Tap to learn more.</string>
|
||||
<string name="notification_new_chapters">New chapters found</string>
|
||||
<plurals name="notification_new_chapters_summary">
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
package tachiyomi.presentation.core.icons
|
||||
|
||||
/**
|
||||
* Icons imported from https://simpleicons.org using
|
||||
* https://github.com/DevSrSouza/svg-to-compose
|
||||
*/
|
||||
object CustomIcons
|
|
@ -0,0 +1,76 @@
|
|||
package tachiyomi.presentation.core.icons
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.PathFillType.Companion.NonZero
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.StrokeCap.Companion.Butt
|
||||
import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
val CustomIcons.Discord: ImageVector
|
||||
get() {
|
||||
if (_discord != null) {
|
||||
return _discord!!
|
||||
}
|
||||
_discord = ImageVector.Builder(
|
||||
name = "Discord", defaultWidth = 24.0.dp, defaultHeight = 24.0.dp,
|
||||
viewportWidth = 24.0f, viewportHeight = 24.0f,
|
||||
).apply {
|
||||
path(
|
||||
fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f,
|
||||
strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f,
|
||||
pathFillType = NonZero,
|
||||
) {
|
||||
moveTo(20.317f, 4.3698f)
|
||||
arcToRelative(19.7913f, 19.7913f, 0.0f, false, false, -4.8851f, -1.5152f)
|
||||
arcToRelative(0.0741f, 0.0741f, 0.0f, false, false, -0.0785f, 0.0371f)
|
||||
curveToRelative(-0.211f, 0.3753f, -0.4447f, 0.8648f, -0.6083f, 1.2495f)
|
||||
curveToRelative(-1.8447f, -0.2762f, -3.68f, -0.2762f, -5.4868f, 0.0f)
|
||||
curveToRelative(-0.1636f, -0.3933f, -0.4058f, -0.8742f, -0.6177f, -1.2495f)
|
||||
arcToRelative(0.077f, 0.077f, 0.0f, false, false, -0.0785f, -0.037f)
|
||||
arcToRelative(19.7363f, 19.7363f, 0.0f, false, false, -4.8852f, 1.515f)
|
||||
arcToRelative(0.0699f, 0.0699f, 0.0f, false, false, -0.0321f, 0.0277f)
|
||||
curveTo(0.5334f, 9.0458f, -0.319f, 13.5799f, 0.0992f, 18.0578f)
|
||||
arcToRelative(0.0824f, 0.0824f, 0.0f, false, false, 0.0312f, 0.0561f)
|
||||
curveToRelative(2.0528f, 1.5076f, 4.0413f, 2.4228f, 5.9929f, 3.0294f)
|
||||
arcToRelative(0.0777f, 0.0777f, 0.0f, false, false, 0.0842f, -0.0276f)
|
||||
curveToRelative(0.4616f, -0.6304f, 0.8731f, -1.2952f, 1.226f, -1.9942f)
|
||||
arcToRelative(0.076f, 0.076f, 0.0f, false, false, -0.0416f, -0.1057f)
|
||||
curveToRelative(-0.6528f, -0.2476f, -1.2743f, -0.5495f, -1.8722f, -0.8923f)
|
||||
arcToRelative(0.077f, 0.077f, 0.0f, false, true, -0.0076f, -0.1277f)
|
||||
curveToRelative(0.1258f, -0.0943f, 0.2517f, -0.1923f, 0.3718f, -0.2914f)
|
||||
arcToRelative(0.0743f, 0.0743f, 0.0f, false, true, 0.0776f, -0.0105f)
|
||||
curveToRelative(3.9278f, 1.7933f, 8.18f, 1.7933f, 12.0614f, 0.0f)
|
||||
arcToRelative(0.0739f, 0.0739f, 0.0f, false, true, 0.0785f, 0.0095f)
|
||||
curveToRelative(0.1202f, 0.099f, 0.246f, 0.1981f, 0.3728f, 0.2924f)
|
||||
arcToRelative(0.077f, 0.077f, 0.0f, false, true, -0.0066f, 0.1276f)
|
||||
arcToRelative(12.2986f, 12.2986f, 0.0f, false, true, -1.873f, 0.8914f)
|
||||
arcToRelative(0.0766f, 0.0766f, 0.0f, false, false, -0.0407f, 0.1067f)
|
||||
curveToRelative(0.3604f, 0.698f, 0.7719f, 1.3628f, 1.225f, 1.9932f)
|
||||
arcToRelative(0.076f, 0.076f, 0.0f, false, false, 0.0842f, 0.0286f)
|
||||
curveToRelative(1.961f, -0.6067f, 3.9495f, -1.5219f, 6.0023f, -3.0294f)
|
||||
arcToRelative(0.077f, 0.077f, 0.0f, false, false, 0.0313f, -0.0552f)
|
||||
curveToRelative(0.5004f, -5.177f, -0.8382f, -9.6739f, -3.5485f, -13.6604f)
|
||||
arcToRelative(0.061f, 0.061f, 0.0f, false, false, -0.0312f, -0.0286f)
|
||||
close()
|
||||
moveTo(8.02f, 15.3312f)
|
||||
curveToRelative(-1.1825f, 0.0f, -2.1569f, -1.0857f, -2.1569f, -2.419f)
|
||||
curveToRelative(0.0f, -1.3332f, 0.9555f, -2.4189f, 2.157f, -2.4189f)
|
||||
curveToRelative(1.2108f, 0.0f, 2.1757f, 1.0952f, 2.1568f, 2.419f)
|
||||
curveToRelative(0.0f, 1.3332f, -0.9555f, 2.4189f, -2.1569f, 2.4189f)
|
||||
close()
|
||||
moveTo(15.9948f, 15.3312f)
|
||||
curveToRelative(-1.1825f, 0.0f, -2.1569f, -1.0857f, -2.1569f, -2.419f)
|
||||
curveToRelative(0.0f, -1.3332f, 0.9554f, -2.4189f, 2.1569f, -2.4189f)
|
||||
curveToRelative(1.2108f, 0.0f, 2.1757f, 1.0952f, 2.1568f, 2.419f)
|
||||
curveToRelative(0.0f, 1.3332f, -0.946f, 2.4189f, -2.1568f, 2.4189f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
.build()
|
||||
return _discord!!
|
||||
}
|
||||
|
||||
private var _discord: ImageVector? = null
|
|
@ -0,0 +1,59 @@
|
|||
package tachiyomi.presentation.core.icons
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.PathFillType.Companion.NonZero
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.StrokeCap.Companion.Butt
|
||||
import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
val CustomIcons.Github: ImageVector
|
||||
get() {
|
||||
if (_github != null) {
|
||||
return _github!!
|
||||
}
|
||||
_github = Builder(
|
||||
name = "Github", defaultWidth = 24.0.dp, defaultHeight = 24.0.dp,
|
||||
viewportWidth = 24.0f, viewportHeight = 24.0f,
|
||||
).apply {
|
||||
path(
|
||||
fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f,
|
||||
strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f,
|
||||
pathFillType = NonZero,
|
||||
) {
|
||||
moveTo(12.0f, 0.297f)
|
||||
curveToRelative(-6.63f, 0.0f, -12.0f, 5.373f, -12.0f, 12.0f)
|
||||
curveToRelative(0.0f, 5.303f, 3.438f, 9.8f, 8.205f, 11.385f)
|
||||
curveToRelative(0.6f, 0.113f, 0.82f, -0.258f, 0.82f, -0.577f)
|
||||
curveToRelative(0.0f, -0.285f, -0.01f, -1.04f, -0.015f, -2.04f)
|
||||
curveToRelative(-3.338f, 0.724f, -4.042f, -1.61f, -4.042f, -1.61f)
|
||||
curveTo(4.422f, 18.07f, 3.633f, 17.7f, 3.633f, 17.7f)
|
||||
curveToRelative(-1.087f, -0.744f, 0.084f, -0.729f, 0.084f, -0.729f)
|
||||
curveToRelative(1.205f, 0.084f, 1.838f, 1.236f, 1.838f, 1.236f)
|
||||
curveToRelative(1.07f, 1.835f, 2.809f, 1.305f, 3.495f, 0.998f)
|
||||
curveToRelative(0.108f, -0.776f, 0.417f, -1.305f, 0.76f, -1.605f)
|
||||
curveToRelative(-2.665f, -0.3f, -5.466f, -1.332f, -5.466f, -5.93f)
|
||||
curveToRelative(0.0f, -1.31f, 0.465f, -2.38f, 1.235f, -3.22f)
|
||||
curveToRelative(-0.135f, -0.303f, -0.54f, -1.523f, 0.105f, -3.176f)
|
||||
curveToRelative(0.0f, 0.0f, 1.005f, -0.322f, 3.3f, 1.23f)
|
||||
curveToRelative(0.96f, -0.267f, 1.98f, -0.399f, 3.0f, -0.405f)
|
||||
curveToRelative(1.02f, 0.006f, 2.04f, 0.138f, 3.0f, 0.405f)
|
||||
curveToRelative(2.28f, -1.552f, 3.285f, -1.23f, 3.285f, -1.23f)
|
||||
curveToRelative(0.645f, 1.653f, 0.24f, 2.873f, 0.12f, 3.176f)
|
||||
curveToRelative(0.765f, 0.84f, 1.23f, 1.91f, 1.23f, 3.22f)
|
||||
curveToRelative(0.0f, 4.61f, -2.805f, 5.625f, -5.475f, 5.92f)
|
||||
curveToRelative(0.42f, 0.36f, 0.81f, 1.096f, 0.81f, 2.22f)
|
||||
curveToRelative(0.0f, 1.606f, -0.015f, 2.896f, -0.015f, 3.286f)
|
||||
curveToRelative(0.0f, 0.315f, 0.21f, 0.69f, 0.825f, 0.57f)
|
||||
curveTo(20.565f, 22.092f, 24.0f, 17.592f, 24.0f, 12.297f)
|
||||
curveToRelative(0.0f, -6.627f, -5.373f, -12.0f, -12.0f, -12.0f)
|
||||
}
|
||||
}
|
||||
.build()
|
||||
return _github!!
|
||||
}
|
||||
|
||||
private var _github: ImageVector? = null
|
|
@ -0,0 +1,85 @@
|
|||
package tachiyomi.presentation.core.icons
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.PathFillType.Companion.NonZero
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.StrokeCap.Companion.Butt
|
||||
import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
val CustomIcons.Reddit: ImageVector
|
||||
get() {
|
||||
if (_reddit != null) {
|
||||
return _reddit!!
|
||||
}
|
||||
_reddit = Builder(
|
||||
name = "Reddit", defaultWidth = 24.0.dp, defaultHeight = 24.0.dp,
|
||||
viewportWidth = 24.0f, viewportHeight = 24.0f,
|
||||
).apply {
|
||||
path(
|
||||
fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f,
|
||||
strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f,
|
||||
pathFillType = NonZero,
|
||||
) {
|
||||
moveTo(12.0f, 0.0f)
|
||||
arcTo(12.0f, 12.0f, 0.0f, false, false, 0.0f, 12.0f)
|
||||
arcToRelative(12.0f, 12.0f, 0.0f, false, false, 12.0f, 12.0f)
|
||||
arcToRelative(12.0f, 12.0f, 0.0f, false, false, 12.0f, -12.0f)
|
||||
arcTo(12.0f, 12.0f, 0.0f, false, false, 12.0f, 0.0f)
|
||||
close()
|
||||
moveTo(17.01f, 4.744f)
|
||||
curveToRelative(0.688f, 0.0f, 1.25f, 0.561f, 1.25f, 1.249f)
|
||||
arcToRelative(1.25f, 1.25f, 0.0f, false, true, -2.498f, 0.056f)
|
||||
lineToRelative(-2.597f, -0.547f)
|
||||
lineToRelative(-0.8f, 3.747f)
|
||||
curveToRelative(1.824f, 0.07f, 3.48f, 0.632f, 4.674f, 1.488f)
|
||||
curveToRelative(0.308f, -0.309f, 0.73f, -0.491f, 1.207f, -0.491f)
|
||||
curveToRelative(0.968f, 0.0f, 1.754f, 0.786f, 1.754f, 1.754f)
|
||||
curveToRelative(0.0f, 0.716f, -0.435f, 1.333f, -1.01f, 1.614f)
|
||||
arcToRelative(3.111f, 3.111f, 0.0f, false, true, 0.042f, 0.52f)
|
||||
curveToRelative(0.0f, 2.694f, -3.13f, 4.87f, -7.004f, 4.87f)
|
||||
curveToRelative(-3.874f, 0.0f, -7.004f, -2.176f, -7.004f, -4.87f)
|
||||
curveToRelative(0.0f, -0.183f, 0.015f, -0.366f, 0.043f, -0.534f)
|
||||
arcTo(1.748f, 1.748f, 0.0f, false, true, 4.028f, 12.0f)
|
||||
curveToRelative(0.0f, -0.968f, 0.786f, -1.754f, 1.754f, -1.754f)
|
||||
curveToRelative(0.463f, 0.0f, 0.898f, 0.196f, 1.207f, 0.49f)
|
||||
curveToRelative(1.207f, -0.883f, 2.878f, -1.43f, 4.744f, -1.487f)
|
||||
lineToRelative(0.885f, -4.182f)
|
||||
arcToRelative(0.342f, 0.342f, 0.0f, false, true, 0.14f, -0.197f)
|
||||
arcToRelative(0.35f, 0.35f, 0.0f, false, true, 0.238f, -0.042f)
|
||||
lineToRelative(2.906f, 0.617f)
|
||||
arcToRelative(1.214f, 1.214f, 0.0f, false, true, 1.108f, -0.701f)
|
||||
close()
|
||||
moveTo(9.25f, 12.0f)
|
||||
curveTo(8.561f, 12.0f, 8.0f, 12.562f, 8.0f, 13.25f)
|
||||
curveToRelative(0.0f, 0.687f, 0.561f, 1.248f, 1.25f, 1.248f)
|
||||
curveToRelative(0.687f, 0.0f, 1.248f, -0.561f, 1.248f, -1.249f)
|
||||
curveToRelative(0.0f, -0.688f, -0.561f, -1.249f, -1.249f, -1.249f)
|
||||
close()
|
||||
moveTo(14.75f, 12.0f)
|
||||
curveToRelative(-0.687f, 0.0f, -1.248f, 0.561f, -1.248f, 1.25f)
|
||||
curveToRelative(0.0f, 0.687f, 0.561f, 1.248f, 1.249f, 1.248f)
|
||||
curveToRelative(0.688f, 0.0f, 1.249f, -0.561f, 1.249f, -1.249f)
|
||||
curveToRelative(0.0f, -0.687f, -0.562f, -1.249f, -1.25f, -1.249f)
|
||||
close()
|
||||
moveTo(9.284f, 15.99f)
|
||||
arcToRelative(0.327f, 0.327f, 0.0f, false, false, -0.231f, 0.094f)
|
||||
arcToRelative(0.33f, 0.33f, 0.0f, false, false, 0.0f, 0.463f)
|
||||
curveToRelative(0.842f, 0.842f, 2.484f, 0.913f, 2.961f, 0.913f)
|
||||
curveToRelative(0.477f, 0.0f, 2.105f, -0.056f, 2.961f, -0.913f)
|
||||
arcToRelative(0.361f, 0.361f, 0.0f, false, false, 0.029f, -0.463f)
|
||||
arcToRelative(0.33f, 0.33f, 0.0f, false, false, -0.464f, 0.0f)
|
||||
curveToRelative(-0.547f, 0.533f, -1.684f, 0.73f, -2.512f, 0.73f)
|
||||
curveToRelative(-0.828f, 0.0f, -1.979f, -0.196f, -2.512f, -0.73f)
|
||||
arcToRelative(0.326f, 0.326f, 0.0f, false, false, -0.232f, -0.095f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
.build()
|
||||
return _reddit!!
|
||||
}
|
||||
|
||||
private var _reddit: ImageVector? = null
|
Loading…
Reference in a new issue