Last commit merged: eed57b80be
This commit is contained in:
LuftVerbot 2023-11-25 13:38:22 +01:00
parent 5f782440c6
commit c6f81b7fda
114 changed files with 1946 additions and 1175 deletions

View file

@ -40,7 +40,7 @@ Please make sure to read the full guidelines. Your issue may be closed without w
* Include version (More → About → Version)
* If not latest, try updating, it may have already been solved
* Preview version is equal to the number of commits as seen in the main page
* Preview version is equal to the number of commits as seen on the main page
* Include steps to reproduce (if not obvious from description)
* Include screenshot (if needed)
* If it could be device-dependent, try reproducing on another device (if possible)

View file

@ -207,10 +207,6 @@
android:name=".data.download.anime.AnimeDownloadService"
android:exported="false" />
<service
android:name=".data.updater.AppUpdateService"
android:exported="false" />
<service
android:name=".extension.manga.util.MangaExtensionInstallService"
android:exported="false" />

View file

@ -85,10 +85,12 @@ import tachiyomi.domain.entries.anime.interactor.GetAnimeFavorites
import tachiyomi.domain.entries.anime.interactor.GetAnimeWithEpisodes
import tachiyomi.domain.entries.anime.interactor.GetDuplicateLibraryAnime
import tachiyomi.domain.entries.anime.interactor.GetLibraryAnime
import tachiyomi.domain.entries.anime.interactor.GetMangaByUrlAndSourceId
import tachiyomi.domain.entries.anime.interactor.NetworkToLocalAnime
import tachiyomi.domain.entries.anime.interactor.ResetAnimeViewerFlags
import tachiyomi.domain.entries.anime.interactor.SetAnimeEpisodeFlags
import tachiyomi.domain.entries.anime.repository.AnimeRepository
import tachiyomi.domain.entries.manga.interactor.GetAnimeByUrlAndSourceId
import tachiyomi.domain.entries.manga.interactor.GetDuplicateLibraryManga
import tachiyomi.domain.entries.manga.interactor.GetLibraryManga
import tachiyomi.domain.entries.manga.interactor.GetManga
@ -112,12 +114,14 @@ import tachiyomi.domain.history.manga.interactor.UpsertMangaHistory
import tachiyomi.domain.history.manga.repository.MangaHistoryRepository
import tachiyomi.domain.items.chapter.interactor.GetChapter
import tachiyomi.domain.items.chapter.interactor.GetChapterByMangaId
import tachiyomi.domain.items.chapter.interactor.GetChapterByUrlAndMangaId
import tachiyomi.domain.items.chapter.interactor.SetMangaDefaultChapterFlags
import tachiyomi.domain.items.chapter.interactor.ShouldUpdateDbChapter
import tachiyomi.domain.items.chapter.interactor.UpdateChapter
import tachiyomi.domain.items.chapter.repository.ChapterRepository
import tachiyomi.domain.items.episode.interactor.GetEpisode
import tachiyomi.domain.items.episode.interactor.GetEpisodeByAnimeId
import tachiyomi.domain.items.episode.interactor.GetEpisodeByUrlAndAnimeId
import tachiyomi.domain.items.episode.interactor.SetAnimeDefaultEpisodeFlags
import tachiyomi.domain.items.episode.interactor.ShouldUpdateDbEpisode
import tachiyomi.domain.items.episode.interactor.UpdateEpisode
@ -186,6 +190,7 @@ class DomainModule : InjektModule {
addFactory { GetAnimeFavorites(get()) }
addFactory { GetLibraryAnime(get()) }
addFactory { GetAnimeWithEpisodes(get(), get()) }
addFactory { GetAnimeByUrlAndSourceId(get()) }
addFactory { GetAnime(get()) }
addFactory { GetNextEpisodes(get(), get(), get()) }
addFactory { ResetAnimeViewerFlags(get()) }
@ -202,6 +207,7 @@ class DomainModule : InjektModule {
addFactory { GetMangaFavorites(get()) }
addFactory { GetLibraryManga(get()) }
addFactory { GetMangaWithChapters(get(), get()) }
addFactory { GetMangaByUrlAndSourceId(get()) }
addFactory { GetManga(get()) }
addFactory { GetNextChapters(get(), get(), get()) }
addFactory { ResetMangaViewerFlags(get()) }
@ -245,6 +251,7 @@ class DomainModule : InjektModule {
addSingletonFactory<EpisodeRepository> { EpisodeRepositoryImpl(get()) }
addFactory { GetEpisode(get()) }
addFactory { GetEpisodeByAnimeId(get()) }
addFactory { GetEpisodeByUrlAndAnimeId(get()) }
addFactory { UpdateEpisode(get()) }
addFactory { SetSeenStatus(get(), get(), get(), get()) }
addFactory { ShouldUpdateDbEpisode() }
@ -253,6 +260,7 @@ class DomainModule : InjektModule {
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
addFactory { GetChapter(get()) }
addFactory { GetChapterByMangaId(get()) }
addFactory { GetChapterByUrlAndMangaId(get()) }
addFactory { UpdateChapter(get()) }
addFactory { SetReadStatus(get(), get(), get(), get()) }
addFactory { ShouldUpdateDbChapter() }

View file

@ -54,7 +54,7 @@ fun GlobalSearchResultItem(
Text(text = subtitle)
}
IconButton(onClick = onClick) {
Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null)
Icon(imageVector = Icons.Outlined.ArrowForward, contentDescription = null)
}
}
content()

View file

@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.History
import androidx.compose.material.icons.outlined.Settings
@ -92,7 +91,7 @@ fun AnimeExtensionDetailsScreen(
add(
AppBar.Action(
title = stringResource(R.string.action_faq_and_guides),
icon = Icons.AutoMirrored.Outlined.HelpOutline,
icon = Icons.Outlined.HelpOutline,
onClick = onClickReadme,
),
)

View file

@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.Public
import androidx.compose.material.icons.outlined.Refresh
@ -80,7 +79,7 @@ fun BrowseAnimeSourceContent(
listOf(
EmptyScreenAction(
stringResId = R.string.local_source_help_guide,
icon = Icons.AutoMirrored.Outlined.HelpOutline,
icon = Icons.Outlined.HelpOutline,
onClick = onLocalAnimeSourceHelpClick,
),
)
@ -98,7 +97,7 @@ fun BrowseAnimeSourceContent(
),
EmptyScreenAction(
stringResId = R.string.label_help,
icon = Icons.AutoMirrored.Outlined.HelpOutline,
icon = Icons.Outlined.HelpOutline,
onClick = onHelpClick,
),
)

View file

@ -56,7 +56,7 @@ fun BrowseAnimeSourceToolbar(
actions = listOfNotNull(
AppBar.Action(
title = stringResource(R.string.action_display_mode),
icon = if (displayMode == LibraryDisplayMode.List) Icons.AutoMirrored.Filled.ViewList else Icons.Filled.ViewModule,
icon = if (displayMode == LibraryDisplayMode.List) Icons.Filled.ViewList else Icons.Filled.ViewModule,
onClick = { selectingDisplayMode = true },
),
if (isLocalSource) {

View file

@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.Public
import androidx.compose.material.icons.outlined.Refresh
@ -80,7 +79,7 @@ fun BrowseSourceContent(
listOf(
EmptyScreenAction(
stringResId = R.string.local_source_help_guide,
icon = Icons.AutoMirrored.Outlined.HelpOutline,
icon = Icons.Outlined.HelpOutline,
onClick = onLocalSourceHelpClick,
),
)
@ -98,7 +97,7 @@ fun BrowseSourceContent(
),
EmptyScreenAction(
stringResId = R.string.label_help,
icon = Icons.AutoMirrored.Outlined.HelpOutline,
icon = Icons.Outlined.HelpOutline,
onClick = onHelpClick,
),
)

View file

@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.History
import androidx.compose.material.icons.outlined.Settings
@ -93,7 +92,7 @@ fun ExtensionDetailsScreen(
add(
AppBar.Action(
title = stringResource(R.string.action_faq_and_guides),
icon = Icons.AutoMirrored.Outlined.HelpOutline,
icon = Icons.Outlined.HelpOutline,
onClick = onClickReadme,
),
)

View file

@ -56,7 +56,7 @@ fun BrowseMangaSourceToolbar(
actions = listOfNotNull(
AppBar.Action(
title = stringResource(R.string.action_display_mode),
icon = if (displayMode == LibraryDisplayMode.List) Icons.AutoMirrored.Filled.ViewList else Icons.Filled.ViewModule,
icon = if (displayMode == LibraryDisplayMode.List) Icons.Filled.ViewList else Icons.Filled.ViewModule,
onClick = { selectingDisplayMode = true },
),
if (isLocalSource) {

View file

@ -58,7 +58,7 @@ fun GlobalMangaSearchToolbar(
)
if (progress in 1..<total) {
LinearProgressIndicator(
progress = { progress / total.toFloat() },
progress = progress / total.toFloat(),
modifier = Modifier
.align(Alignment.BottomStart)
.fillMaxWidth(),

View file

@ -52,7 +52,7 @@ fun CategoryListItem(
),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = "")
Icon(imageVector = Icons.Outlined.Label, contentDescription = "")
Text(
text = category.name,
modifier = Modifier

View file

@ -13,7 +13,6 @@ import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.TextFieldDefaults
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.MoreVert
@ -401,7 +400,7 @@ fun SearchToolbar(
@Composable
fun UpIcon(navigationIcon: ImageVector? = null) {
val icon = navigationIcon
?: Icons.AutoMirrored.Outlined.ArrowBack
?: Icons.Outlined.ArrowBack
Icon(
imageVector = icon,
contentDescription = stringResource(R.string.abc_action_bar_up_description),

View file

@ -3,7 +3,7 @@ package eu.kanade.presentation.components
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
import androidx.compose.material.icons.outlined.ArrowLeft
import androidx.compose.material.icons.outlined.ArrowRight
import androidx.compose.material.icons.outlined.RadioButtonChecked
import androidx.compose.material.icons.outlined.RadioButtonUnchecked
@ -84,7 +84,7 @@ fun NestedMenuItem(
onClick = { nestedExpanded = true },
trailingIcon = {
Icon(
imageVector = Icons.AutoMirrored.Outlined.ArrowRight,
imageVector = if (isLtr) Icons.Outlined.ArrowRight else Icons.Outlined.ArrowLeft,
contentDescription = null,
)
},

View file

@ -1,7 +1,6 @@
package eu.kanade.presentation.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.material3.Surface
@ -39,7 +38,7 @@ private fun WithActionPreview() {
),
EmptyScreenAction(
stringResId = R.string.getting_started_guide,
icon = Icons.AutoMirrored.Outlined.HelpOutline,
icon = Icons.Outlined.HelpOutline,
onClick = {},
),
),

View file

@ -14,8 +14,8 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -60,7 +60,7 @@ fun TabbedDialog(
Column {
Row {
PrimaryTabRow(
TabRow(
modifier = Modifier.weight(1f),
selectedTabIndex = pagerState.currentPage,
indicator = {

View file

@ -10,8 +10,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.TabRow
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Tab
@ -142,7 +142,7 @@ private fun FlexibleTabRow(
block: @Composable () -> Unit,
) {
return if (scrollable) {
PrimaryScrollableTabRow(
ScrollableTabRow(
selectedTabIndex = selectedTabIndex,
indicator = indicator,
edgePadding = 13.dp,
@ -150,7 +150,7 @@ private fun FlexibleTabRow(
block()
}
} else {
PrimaryTabRow(
TabRow(
selectedTabIndex = selectedTabIndex,
indicator = indicator,
) {

View file

@ -296,7 +296,7 @@ fun LibraryBottomActionMenu(
) {
Button(
title = stringResource(R.string.action_move_category),
icon = Icons.AutoMirrored.Outlined.Label,
icon = Icons.Outlined.Label,
toConfirm = confirm[0],
onLongClick = { onLongClickItem(0) },
onClick = onChangeCategoryClicked,

View file

@ -145,7 +145,7 @@ private fun DownloadingIndicator(
MaterialTheme.colorScheme.background
}
CircularProgressIndicator(
progress = { animatedProgress },
progress = animatedProgress,
modifier = IndicatorModifier,
color = strokeColor,
strokeWidth = IndicatorSize / 2,

View file

@ -144,7 +144,7 @@ private fun DownloadingIndicator(
MaterialTheme.colorScheme.background
}
CircularProgressIndicator(
progress = { animatedProgress },
progress = animatedProgress,
modifier = IndicatorModifier,
color = strokeColor,
strokeWidth = IndicatorSize / 2,

View file

@ -1,20 +0,0 @@
package eu.kanade.presentation.extensions
import android.Manifest
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import com.google.accompanist.permissions.rememberPermissionState
import eu.kanade.tachiyomi.util.storage.DiskUtil
/**
* Launches request for [Manifest.permission.WRITE_EXTERNAL_STORAGE] permission
*/
@Composable
fun DiskUtil.RequestStoragePermission() {
val permissionState = rememberPermissionState(
permission = Manifest.permission.WRITE_EXTERNAL_STORAGE,
)
LaunchedEffect(Unit) {
permissionState.launchPermissionRequest()
}
}

View file

@ -4,7 +4,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.pager.PagerState
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.Tab
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
@ -21,7 +21,7 @@ fun LibraryTabs(
onTabItemClick: (Int) -> Unit,
) {
Column {
PrimaryScrollableTabRow(
ScrollableTabRow(
selectedTabIndex = pagerState.currentPage,
edgePadding = 0.dp,
indicator = {

View file

@ -9,8 +9,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.automirrored.outlined.Label
import androidx.compose.material.icons.outlined.CloudOff
import androidx.compose.material.icons.outlined.CollectionsBookmark
import androidx.compose.material.icons.outlined.GetApp
@ -54,7 +52,7 @@ fun MoreScreen(
onClickCategories: () -> Unit,
onClickStats: () -> Unit,
onClickStorage: () -> Unit,
onClickBackupAndRestore: () -> Unit,
onClickDataAndStorage: () -> Unit,
onClickSettings: () -> Unit,
onClickAbout: () -> Unit,
) {
@ -166,7 +164,7 @@ fun MoreScreen(
item {
TextPreferenceWidget(
title = stringResource(R.string.general_categories),
icon = Icons.AutoMirrored.Outlined.Label,
icon = Icons.Outlined.Label,
onPreferenceClick = onClickCategories,
)
}
@ -186,9 +184,9 @@ fun MoreScreen(
}
item {
TextPreferenceWidget(
title = stringResource(R.string.label_backup),
icon = Icons.Outlined.SettingsBackupRestore,
onPreferenceClick = onClickBackupAndRestore,
title = stringResource(R.string.label_data_storage),
icon = Icons.Outlined.Storage,
onPreferenceClick = onClickDataAndStorage,
)
}
@ -211,7 +209,7 @@ fun MoreScreen(
item {
TextPreferenceWidget(
title = stringResource(R.string.label_help),
icon = Icons.AutoMirrored.Outlined.HelpOutline,
icon = Icons.Outlined.HelpOutline,
onPreferenceClick = { uriHandler.openUri(Constants.URL_HELP) },
)
}

View file

@ -5,9 +5,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.OpenInNew
import androidx.compose.material.icons.filled.OpenInNew
import androidx.compose.material.icons.outlined.NewReleases
import androidx.compose.material.icons.outlined.OpenInNew
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@ -61,7 +61,7 @@ fun NewUpdateScreen(
) {
Text(text = stringResource(R.string.update_check_open))
Spacer(modifier = Modifier.width(MaterialTheme.padding.tiny))
Icon(imageVector = Icons.AutoMirrored.Outlined.OpenInNew, contentDescription = null)
Icon(imageVector = Icons.Default.OpenInNew, contentDescription = null)
}
}
}

View file

@ -195,48 +195,12 @@ object SettingsAdvancedScreen : SearchableSettings {
@Composable
private fun getDataGroup(): Preference.PreferenceGroup {
val scope = rememberCoroutineScope()
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val libraryPreferences = remember { Injekt.get<LibraryPreferences>() }
val chapterCache = remember { Injekt.get<ChapterCache>() }
val episodeCache = remember { Injekt.get<EpisodeCache>() }
var readableSizeSema by remember { mutableIntStateOf(0) }
val readableSize = remember(readableSizeSema) { chapterCache.readableSize }
val readableAnimeSize = remember(readableSizeSema) { episodeCache.readableSize }
return Preference.PreferenceGroup(
title = stringResource(R.string.label_data),
preferenceItems = listOf(
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_clear_chapter_cache),
subtitle = stringResource(
R.string.used_cache_both,
readableAnimeSize,
readableSize,
),
onClick = {
scope.launchNonCancellable {
try {
val deletedFiles = chapterCache.clear() + episodeCache.clear()
withUIContext {
context.toast(
context.getString(R.string.cache_deleted, deletedFiles),
)
readableSizeSema++
}
} catch (e: Throwable) {
logcat(LogPriority.ERROR, e)
withUIContext { context.toast(R.string.cache_delete_error) }
}
}
},
),
Preference.PreferenceItem.SwitchPreference(
pref = libraryPreferences.autoClearItemCache(),
title = stringResource(R.string.pref_auto_clear_chapter_cache),
),
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_invalidate_download_cache),
subtitle = stringResource(R.string.pref_invalidate_download_cache_summary),

View file

@ -31,21 +31,24 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.core.net.toUri
import com.hippo.unifile.UniFile
import eu.kanade.presentation.extensions.RequestStoragePermission
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.permissions.PermissionRequestHelper
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupConst
import eu.kanade.tachiyomi.data.backup.BackupCreateJob
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
import eu.kanade.tachiyomi.data.backup.BackupRestoreJob
import eu.kanade.tachiyomi.data.backup.models.Backup
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.cache.EpisodeCache
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.launch
import logcat.LogPriority
import tachiyomi.core.util.lang.launchNonCancellable
import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.backup.service.FLAG_CATEGORIES
import tachiyomi.domain.backup.service.FLAG_CHAPTERS
@ -54,6 +57,7 @@ import tachiyomi.domain.backup.service.FLAG_EXT_SETTINGS
import tachiyomi.domain.backup.service.FLAG_HISTORY
import tachiyomi.domain.backup.service.FLAG_SETTINGS
import tachiyomi.domain.backup.service.FLAG_TRACK
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.util.collectAsState
@ -62,23 +66,63 @@ import tachiyomi.presentation.core.util.isScrolledToStart
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
object SettingsBackupScreen : SearchableSettings {
object SettingsDataScreen : SearchableSettings {
@ReadOnlyComposable
@Composable
@StringRes
override fun getTitleRes() = R.string.label_backup
override fun getTitleRes() = R.string.label_data_storage
@Composable
override fun getPreferences(): List<Preference> {
val backupPreferences = Injekt.get<BackupPreferences>()
DiskUtil.RequestStoragePermission()
PermissionRequestHelper.requestStoragePermission()
return listOf(
getCreateBackupPref(),
getRestoreBackupPref(),
getAutomaticBackupGroup(backupPreferences = backupPreferences),
getBackupAndRestoreGroup(backupPreferences = backupPreferences),
getDataGroup(backupPreferences = backupPreferences),
)
}
@Composable
private fun getBackupAndRestoreGroup(backupPreferences: BackupPreferences): Preference.PreferenceGroup {
val context = LocalContext.current
val backupIntervalPref = backupPreferences.backupInterval()
val backupInterval by backupIntervalPref.collectAsState()
return Preference.PreferenceGroup(
title = stringResource(R.string.label_backup),
preferenceItems = listOf(
// Manual actions
getCreateBackupPref(),
getRestoreBackupPref(),
// Automatic backups
Preference.PreferenceItem.ListPreference(
pref = backupIntervalPref,
title = stringResource(R.string.pref_backup_interval),
entries = mapOf(
0 to stringResource(R.string.off),
6 to stringResource(R.string.update_6hour),
12 to stringResource(R.string.update_12hour),
24 to stringResource(R.string.update_24hour),
48 to stringResource(R.string.update_48hour),
168 to stringResource(R.string.update_weekly),
),
onValueChanged = {
BackupCreateJob.setupTask(context, it)
true
},
),
Preference.PreferenceItem.ListPreference(
pref = backupPreferences.numberOfBackups(),
enabled = backupInterval != 0,
title = stringResource(R.string.pref_backup_slots),
entries = listOf(2, 3, 4, 5).associateWith { it.toString() },
),
Preference.PreferenceItem.InfoPreference(stringResource(R.string.backup_info)),
),
)
}
@ -349,68 +393,48 @@ object SettingsBackupScreen : SearchableSettings {
}
@Composable
private fun getAutomaticBackupGroup(
backupPreferences: BackupPreferences,
): Preference.PreferenceGroup {
private fun getDataGroup(backupPreferences: BackupPreferences): Preference.PreferenceGroup {
val scope = rememberCoroutineScope()
val context = LocalContext.current
val libraryPreferences = remember { Injekt.get<LibraryPreferences>() }
val backupIntervalPref = backupPreferences.backupInterval()
val backupInterval by backupIntervalPref.collectAsState()
val backupDirPref = backupPreferences.backupsDirectory()
val backupDir by backupDirPref.collectAsState()
val pickBackupLocation = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocumentTree(),
) { uri ->
if (uri != null) {
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
context.contentResolver.takePersistableUriPermission(uri, flags)
val file = UniFile.fromUri(context, uri)
backupDirPref.set(file.uri.toString())
}
}
val chapterCache = remember { Injekt.get<ChapterCache>() }
val episodeCache = remember { Injekt.get<EpisodeCache>()}
var readableSizeSema by remember { mutableIntStateOf(0) }
val readableSize = remember(readableSizeSema) { chapterCache.readableSize }
val readableAnimeSize = remember(readableSizeSema) { episodeCache.readableSize }
return Preference.PreferenceGroup(
title = stringResource(R.string.pref_backup_service_category),
title = stringResource(R.string.label_data),
preferenceItems = listOf(
Preference.PreferenceItem.ListPreference(
pref = backupIntervalPref,
title = stringResource(R.string.pref_backup_interval),
entries = mapOf(
0 to stringResource(R.string.off),
6 to stringResource(R.string.update_6hour),
12 to stringResource(R.string.update_12hour),
24 to stringResource(R.string.update_24hour),
48 to stringResource(R.string.update_48hour),
168 to stringResource(R.string.update_weekly),
),
onValueChanged = {
BackupCreateJob.setupTask(context, it)
true
},
),
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_backup_directory),
enabled = backupInterval != 0,
subtitle = remember(backupDir) {
(UniFile.fromUri(context, backupDir.toUri())?.filePath)?.let {
"$it/automatic"
}
} ?: stringResource(R.string.invalid_location, backupDir),
title = stringResource(R.string.pref_clear_chapter_cache),
subtitle = stringResource(
R.string.used_cache_both,
readableAnimeSize,
readableSize,
),
onClick = {
try {
pickBackupLocation.launch(null)
} catch (e: ActivityNotFoundException) {
context.toast(R.string.file_picker_error)
scope.launchNonCancellable {
try {
val deletedFiles = chapterCache.clear() + episodeCache.clear()
withUIContext {
context.toast(context.getString(R.string.cache_deleted, deletedFiles))
readableSizeSema++
}
} catch (e: Throwable) {
logcat(LogPriority.ERROR, e)
withUIContext { context.toast(R.string.cache_delete_error) }
}
}
},
),
Preference.PreferenceItem.ListPreference(
pref = backupPreferences.numberOfBackups(),
enabled = backupInterval != 0,
title = stringResource(R.string.pref_backup_slots),
entries = listOf(2, 3, 4, 5).associateWith { it.toString() },
Preference.PreferenceItem.SwitchPreference(
pref = libraryPreferences.autoClearItemCache(),
title = stringResource(R.string.pref_auto_clear_chapter_cache),
),
Preference.PreferenceItem.MultiSelectListPreference(
pref = backupPreferences.backupFlags(),
@ -433,7 +457,6 @@ object SettingsBackupScreen : SearchableSettings {
true
},
),
Preference.PreferenceItem.InfoPreference(stringResource(R.string.backup_info)),
),
)
}

View file

@ -20,6 +20,7 @@ import androidx.compose.material.icons.outlined.PlayCircleOutline
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.Security
import androidx.compose.material.icons.outlined.SettingsBackupRestore
import androidx.compose.material.icons.outlined.Storage
import androidx.compose.material.icons.outlined.Sync
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
@ -188,7 +189,7 @@ object SettingsMainScreen : Screen() {
Item(
titleRes = R.string.pref_category_reader,
subtitleRes = R.string.pref_reader_summary,
icon = Icons.AutoMirrored.Outlined.ChromeReaderMode,
icon = Icons.Outlined.ChromeReaderMode,
screen = SettingsReaderScreen,
),
Item(
@ -216,10 +217,10 @@ object SettingsMainScreen : Screen() {
screen = SettingsBrowseScreen,
),
Item(
titleRes = R.string.label_backup,
titleRes = R.string.label_data_storage,
subtitleRes = R.string.pref_backup_summary,
icon = Icons.Outlined.SettingsBackupRestore,
screen = SettingsBackupScreen,
icon = Icons.Outlined.Storage,
screen = SettingsDataScreen,
),
Item(
titleRes = R.string.pref_category_security,

View file

@ -301,7 +301,7 @@ private val settingScreens = listOf(
SettingsDownloadScreen,
SettingsTrackingScreen,
SettingsBrowseScreen,
SettingsBackupScreen,
SettingsDataScreen,
SettingsSecurityScreen,
SettingsAdvancedScreen,
AdvancedPlayerSettingsScreen,

View file

@ -0,0 +1,21 @@
package eu.kanade.presentation.permissions
import android.Manifest
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import com.google.accompanist.permissions.rememberPermissionState
/**
* Launches request for [Manifest.permission.WRITE_EXTERNAL_STORAGE] permission
*/
object PermissionRequestHelper {
@Composable
fun requestStoragePermission() {
val permissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE)
LaunchedEffect(Unit) {
permissionState.launchPermissionRequest()
}
}
}

View file

@ -0,0 +1,26 @@
package eu.kanade.presentation.reader
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import kotlin.math.abs
@Composable
fun BrightnessOverlay(
value: Int,
) {
if (value >= 0) return
Canvas(
modifier = Modifier
.fillMaxSize()
.graphicsLayer {
alpha = abs(value) / 100f
},
) {
drawRect(Color.Black)
}
}

View file

@ -15,11 +15,13 @@ import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import eu.kanade.domain.entries.manga.model.orientationType
import eu.kanade.presentation.components.AdaptiveSheet
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
import tachiyomi.presentation.core.components.SettingsIconGrid
import tachiyomi.presentation.core.components.material.IconToggleButton
import tachiyomi.presentation.core.util.ThemePreviews
private val orientationTypeOptions = OrientationType.entries.map { it.stringRes to it }
@ -37,22 +39,46 @@ fun OrientationModeSelectDialog(
}
AdaptiveSheet(onDismissRequest = onDismissRequest) {
Box(modifier = Modifier.padding(vertical = 16.dp)) {
SettingsIconGrid(R.string.rotation_type) {
items(orientationTypeOptions) { (stringRes, mode) ->
IconToggleButton(
checked = mode == orientationType,
onCheckedChange = {
screenModel.onChangeOrientation(mode)
onChange(stringRes)
onDismissRequest()
},
modifier = Modifier.fillMaxWidth(),
imageVector = ImageVector.vectorResource(mode.iconRes),
title = stringResource(stringRes),
)
}
DialogContent(
orientationType = orientationType,
onChangeOrientation = {
screenModel.onChangeOrientation(it)
onChange(it.stringRes)
onDismissRequest()
},
)
}
}
@Composable
private fun DialogContent(
orientationType: OrientationType,
onChangeOrientation: (OrientationType) -> Unit,
) {
Box(modifier = Modifier.padding(vertical = 16.dp)) {
SettingsIconGrid(R.string.rotation_type) {
items(OrientationType.entries) { mode ->
IconToggleButton(
checked = mode == orientationType,
onCheckedChange = {
onChangeOrientation(mode)
},
modifier = Modifier.fillMaxWidth(),
imageVector = ImageVector.vectorResource(mode.iconRes),
title = stringResource(mode.stringRes),
)
}
}
}
}
@ThemePreviews
@Composable
private fun DialogContentPreview() {
TachiyomiTheme {
DialogContent(
orientationType = OrientationType.DEFAULT,
onChangeOrientation = {},
)
}
}

View file

@ -15,12 +15,14 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import eu.kanade.domain.entries.manga.model.readingModeType
import eu.kanade.presentation.components.AdaptiveSheet
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import tachiyomi.presentation.core.components.SettingsIconGrid
import tachiyomi.presentation.core.components.material.IconToggleButton
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.util.ThemePreviews
private val readingModeOptions = ReadingModeType.entries.map { it.stringRes to it }
@ -38,22 +40,47 @@ fun ReadingModeSelectDialog(
}
AdaptiveSheet(onDismissRequest = onDismissRequest) {
Box(modifier = Modifier.padding(vertical = MaterialTheme.padding.medium)) {
SettingsIconGrid(R.string.pref_category_reading_mode) {
items(readingModeOptions) { (stringRes, mode) ->
IconToggleButton(
checked = mode == readingMode,
onCheckedChange = {
screenModel.onChangeReadingMode(mode)
onChange(stringRes)
onDismissRequest()
},
modifier = Modifier.fillMaxWidth(),
imageVector = ImageVector.vectorResource(mode.iconRes),
title = stringResource(stringRes),
)
}
DialogContent(
readingMode = readingMode,
onChangeReadingMode = {
screenModel.onChangeReadingMode(it)
onChange(it.stringRes)
onDismissRequest()
},
)
}
}
@Composable
private fun DialogContent(
readingMode: ReadingModeType,
onChangeReadingMode: (ReadingModeType) -> Unit,
) {
Box(modifier = Modifier.padding(vertical = MaterialTheme.padding.medium)) {
SettingsIconGrid(R.string.pref_category_reading_mode) {
items(ReadingModeType.entries) { mode ->
IconToggleButton(
checked = mode == readingMode,
onCheckedChange = {
onChangeReadingMode(mode)
},
modifier = Modifier.fillMaxWidth(),
imageVector = ImageVector.vectorResource(mode.iconRes),
title = stringResource(mode.stringRes),
)
}
}
}
}
@ThemePreviews
@Composable
private fun DialogContentPreview() {
TachiyomiTheme {
DialogContent(
readingMode = ReadingModeType.DEFAULT,
onChangeReadingMode = {},
)
}
}

View file

@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Bookmark
@ -69,8 +70,8 @@ fun ReaderAppBars(
.surfaceColorAtElevation(3.dp)
.copy(alpha = if (isSystemInDarkTheme()) 0.9f else 0.95f)
val appBarModifier = if (fullscreen) {
Modifier.windowInsetsPadding(WindowInsets.systemBars)
val modifierWithInsetsPadding = if (fullscreen) {
Modifier.systemBarsPadding()
} else {
Modifier
}
@ -91,7 +92,7 @@ fun ReaderAppBars(
),
) {
AppBar(
modifier = appBarModifier
modifier = modifierWithInsetsPadding
.clickable(onClick = onClickTopAppBar),
backgroundColor = backgroundColor,
title = mangaTitle,
@ -137,6 +138,7 @@ fun ReaderAppBars(
),
) {
Column(
modifier = modifierWithInsetsPadding,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
ChapterNavigator(

View file

@ -32,12 +32,14 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.components.WheelNumberPicker
import tachiyomi.presentation.core.components.WheelTextPicker
import tachiyomi.presentation.core.components.material.AlertDialogContent
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.util.ThemePreviews
import tachiyomi.presentation.core.util.isScrolledToEnd
import tachiyomi.presentation.core.util.isScrolledToStart
@ -235,3 +237,27 @@ fun BaseSelector(
},
)
}
@ThemePreviews
@Composable
private fun TrackStatusSelectorPreviews() {
TachiyomiTheme {
TrackStatusSelector(
selection = 1,
onSelectionChange = {},
selections = mapOf(
// Anilist values
1 to R.string.reading,
2 to R.string.plan_to_read,
3 to R.string.completed,
4 to R.string.on_hold,
5 to R.string.dropped,
6 to R.string.repeating,
7 to R.string.watching,
8 to R.string.plan_to_watch,
),
onConfirm = {},
onDismissRequest = {},
)
}
}

View file

@ -32,8 +32,10 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import eu.kanade.domain.track.anime.model.toDbTrack
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.presentation.track.components.TrackLogoIcon
import eu.kanade.presentation.track.manga.TrackDetailsItem
import eu.kanade.presentation.track.manga.TrackInfoItemMenu
@ -41,6 +43,7 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.ui.entries.anime.track.AnimeTrackItem
import eu.kanade.tachiyomi.util.system.copyToClipboard
import tachiyomi.presentation.core.util.ThemePreviews
import java.text.DateFormat
private const val UnsetStatusTextAlpha = 0.5F
@ -161,6 +164,7 @@ private fun TrackInfoItem(
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
)
}
VerticalDivider()
@ -245,3 +249,12 @@ private fun TrackInfoItemEmpty(
}
}
}
@ThemePreviews
@Composable
private fun TrackInfoDialogHomePreviews(
@PreviewParameter(AnimeTrackInfoDialogHomePreviewProvider::class)
content: @Composable () -> Unit,
) {
TachiyomiTheme { content() }
}

View file

@ -0,0 +1,81 @@
package eu.kanade.presentation.track.anime
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import eu.kanade.tachiyomi.dev.preview.DummyTracker
import eu.kanade.tachiyomi.ui.entries.anime.track.AnimeTrackItem
import tachiyomi.domain.track.anime.model.AnimeTrack
import java.text.DateFormat
internal class AnimeTrackInfoDialogHomePreviewProvider :
PreviewParameterProvider<@Composable () -> Unit> {
private val aTrack = AnimeTrack(
id = 1L,
animeId = 2L,
syncId = 3L,
remoteId = 4L,
libraryId = null,
title = "Manage Name On Tracker Site",
lastEpisodeSeen = 2.0,
totalEpisodes = 12L,
status = 1L,
score = 2.0,
remoteUrl = "https://example.com",
startDate = 0L,
finishDate = 0L,
)
private val trackItemWithoutTrack = AnimeTrackItem(
track = null,
tracker = DummyTracker(
id = 1L,
name = "Example Tracker",
),
)
private val trackItemWithTrack = AnimeTrackItem(
track = aTrack,
tracker = DummyTracker(
id = 2L,
name = "Example Tracker 2",
),
)
private val trackersWithAndWithoutTrack = @Composable {
AnimeTrackInfoDialogHome(
trackItems = listOf(
trackItemWithoutTrack,
trackItemWithTrack,
),
dateFormat = DateFormat.getDateInstance(),
onStatusClick = {},
onEpisodeClick = {},
onScoreClick = {},
onStartDateEdit = {},
onEndDateEdit = {},
onNewSearch = {},
onOpenInBrowser = {},
onRemoved = {},
)
}
private val noTrackers = @Composable {
AnimeTrackInfoDialogHome(
trackItems = listOf(),
dateFormat = DateFormat.getDateInstance(),
onStatusClick = {},
onEpisodeClick = {},
onScoreClick = {},
onStartDateEdit = {},
onEndDateEdit = {},
onNewSearch = {},
onOpenInBrowser = {},
onRemoved = {},
)
}
override val values: Sequence<@Composable () -> Unit>
get() = sequenceOf(
trackersWithAndWithoutTrack,
noTrackers,
)
}

View file

@ -18,9 +18,9 @@ import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.HorizontalDivider
@ -42,7 +42,9 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.toLowerCase
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.presentation.track.manga.SearchResultItem
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch
@ -50,6 +52,7 @@ import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen
import tachiyomi.presentation.core.util.ThemePreviews
import tachiyomi.presentation.core.util.plus
import tachiyomi.presentation.core.util.runOnEnterKeyPressed
@ -78,7 +81,7 @@ fun AnimeTrackerSearch(
navigationIcon = {
IconButton(onClick = onDismissRequest) {
Icon(
imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
imageVector = Icons.Default.ArrowBack,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
@ -197,3 +200,12 @@ fun AnimeTrackerSearch(
}
}
}
@ThemePreviews
@Composable
private fun TrackerSearchPreviews(
@PreviewParameter(AnimeTrackerSearchPreviewProvider::class)
content: @Composable () -> Unit,
) {
TachiyomiTheme { content() }
}

View file

@ -0,0 +1,84 @@
package eu.kanade.presentation.track.anime
import androidx.compose.runtime.Composable
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch
import java.time.Instant
import java.time.temporal.ChronoUnit
import kotlin.random.Random
internal class AnimeTrackerSearchPreviewProvider : PreviewParameterProvider<@Composable () -> Unit> {
private val fullPageWithSecondSelected = @Composable {
val items = someTrackSearches().take(30).toList()
AnimeTrackerSearch(
query = TextFieldValue(text = "search text"),
onQueryChange = {},
onDispatchQuery = {},
queryResult = Result.success(items),
selected = items[1],
onSelectedChange = {},
onConfirmSelection = {},
onDismissRequest = {},
)
}
private val fullPageWithoutSelected = @Composable {
AnimeTrackerSearch(
query = TextFieldValue(text = ""),
onQueryChange = {},
onDispatchQuery = {},
queryResult = Result.success(someTrackSearches().take(30).toList()),
selected = null,
onSelectedChange = {},
onConfirmSelection = {},
onDismissRequest = {},
)
}
private val loading = @Composable {
AnimeTrackerSearch(
query = TextFieldValue(),
onQueryChange = {},
onDispatchQuery = {},
queryResult = null,
selected = null,
onSelectedChange = {},
onConfirmSelection = {},
onDismissRequest = {},
)
}
override val values: Sequence<@Composable () -> Unit> = sequenceOf(
fullPageWithSecondSelected,
fullPageWithoutSelected,
loading,
)
private fun someTrackSearches(): Sequence<AnimeTrackSearch> = sequence {
while (true) {
yield(randTrackSearch())
}
}
private fun randTrackSearch() = AnimeTrackSearch().let {
it.id = Random.nextLong()
it.anime_id = Random.nextLong()
it.sync_id = Random.nextInt()
it.media_id = Random.nextLong()
it.library_id = Random.nextLong()
it.title = lorem((1..10).random()).joinToString()
it.last_episode_seen = (0..100).random().toFloat()
it.total_episodes = (100..1000).random()
it.score = (0..10).random().toFloat()
it.status = Random.nextInt()
it.started_watching_date = 0L
it.finished_watching_date = 0L
it.tracking_url = "https://example.com/tracker-example"
it.cover_url = "https://example.com/cover.png"
it.start_date = Instant.now().minus((1L..365).random(), ChronoUnit.DAYS).toString()
it.summary = lorem((0..40).random()).joinToString()
it
}
private fun lorem(words: Int): Sequence<String> =
LoremIpsum(words).values
}

View file

@ -11,8 +11,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.data.track.Tracker
import tachiyomi.presentation.core.util.ThemePreviews
import tachiyomi.presentation.core.util.clickableNoIndication
@Composable
@ -39,3 +42,17 @@ fun TrackLogoIcon(
)
}
}
@ThemePreviews
@Composable
private fun TrackLogoIconPreviews(
@PreviewParameter(TrackLogoIconPreviewProvider::class)
tracker: Tracker,
) {
TachiyomiTheme {
TrackLogoIcon(
tracker = tracker,
onClick = null,
)
}
}

View file

@ -0,0 +1,21 @@
package eu.kanade.presentation.track.components
import android.graphics.Color
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.dev.preview.DummyTracker
internal class TrackLogoIconPreviewProvider : PreviewParameterProvider<Tracker> {
override val values: Sequence<Tracker>
get() = sequenceOf(
DummyTracker(
id = 1L,
name = "Dummy Tracker",
valLogoColor = Color.rgb(18, 25, 35),
valLogo = R.drawable.ic_tracker_anilist,
),
)
}

View file

@ -44,14 +44,17 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import eu.kanade.domain.track.manga.model.toDbTrack
import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.presentation.track.components.TrackLogoIcon
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.ui.entries.manga.track.MangaTrackItem
import eu.kanade.tachiyomi.util.system.copyToClipboard
import tachiyomi.presentation.core.util.ThemePreviews
import java.text.DateFormat
private const val UnsetStatusTextAlpha = 0.5F
@ -172,6 +175,7 @@ private fun TrackInfoItem(
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
)
}
VerticalDivider()
@ -258,6 +262,7 @@ fun TrackDetailsItem(
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurface,
)
}
}
@ -316,3 +321,12 @@ fun TrackInfoItemMenu(
}
}
}
@ThemePreviews
@Composable
private fun TrackInfoDialogHomePreviews(
@PreviewParameter(MangaTrackInfoDialogHomePreviewProvider::class)
content: @Composable () -> Unit,
) {
TachiyomiTheme { content() }
}

View file

@ -0,0 +1,81 @@
package eu.kanade.presentation.track.manga
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import eu.kanade.tachiyomi.dev.preview.DummyTracker
import eu.kanade.tachiyomi.ui.entries.manga.track.MangaTrackItem
import tachiyomi.domain.track.manga.model.MangaTrack
import java.text.DateFormat
internal class MangaTrackInfoDialogHomePreviewProvider :
PreviewParameterProvider<@Composable () -> Unit> {
private val aTrack = MangaTrack(
id = 1L,
mangaId = 2L,
syncId = 3L,
remoteId = 4L,
libraryId = null,
title = "Manage Name On Tracker Site",
lastChapterRead = 2.0,
totalChapters = 12L,
status = 1L,
score = 2.0,
remoteUrl = "https://example.com",
startDate = 0L,
finishDate = 0L,
)
private val trackItemWithoutTrack = MangaTrackItem(
track = null,
tracker = DummyTracker(
id = 1L,
name = "Example Tracker",
),
)
private val trackItemWithTrack = MangaTrackItem(
track = aTrack,
tracker = DummyTracker(
id = 2L,
name = "Example Tracker 2",
),
)
private val trackersWithAndWithoutTrack = @Composable {
MangaTrackInfoDialogHome(
trackItems = listOf(
trackItemWithoutTrack,
trackItemWithTrack,
),
dateFormat = DateFormat.getDateInstance(),
onStatusClick = {},
onChapterClick = {},
onScoreClick = {},
onStartDateEdit = {},
onEndDateEdit = {},
onNewSearch = {},
onOpenInBrowser = {},
onRemoved = {},
)
}
private val noTrackers = @Composable {
MangaTrackInfoDialogHome(
trackItems = listOf(),
dateFormat = DateFormat.getDateInstance(),
onStatusClick = {},
onChapterClick = {},
onScoreClick = {},
onStartDateEdit = {},
onEndDateEdit = {},
onNewSearch = {},
onOpenInBrowser = {},
onRemoved = {},
)
}
override val values: Sequence<@Composable () -> Unit>
get() = sequenceOf(
trackersWithAndWithoutTrack,
noTrackers,
)
}

View file

@ -28,10 +28,10 @@ import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.HorizontalDivider
@ -57,8 +57,10 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.toLowerCase
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.entries.ItemCover
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.model.MangaTrackSearch
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
@ -66,6 +68,7 @@ import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen
import tachiyomi.presentation.core.util.ThemePreviews
import tachiyomi.presentation.core.util.plus
import tachiyomi.presentation.core.util.runOnEnterKeyPressed
import tachiyomi.presentation.core.util.secondaryItemAlpha
@ -95,7 +98,7 @@ fun MangaTrackerSearch(
navigationIcon = {
IconButton(onClick = onDismissRequest) {
Icon(
imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
imageVector = Icons.Default.ArrowBack,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
@ -322,3 +325,12 @@ fun SearchResultItemDetails(
)
}
}
@ThemePreviews
@Composable
private fun TrackerSearchPreviews(
@PreviewParameter(MangaTrackerSearchPreviewProvider::class)
content: @Composable () -> Unit,
) {
TachiyomiTheme { content() }
}

View file

@ -0,0 +1,84 @@
package eu.kanade.presentation.track.manga
import androidx.compose.runtime.Composable
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
import eu.kanade.tachiyomi.data.track.model.MangaTrackSearch
import java.time.Instant
import java.time.temporal.ChronoUnit
import kotlin.random.Random
internal class MangaTrackerSearchPreviewProvider : PreviewParameterProvider<@Composable () -> Unit> {
private val fullPageWithSecondSelected = @Composable {
val items = someTrackSearches().take(30).toList()
MangaTrackerSearch(
query = TextFieldValue(text = "search text"),
onQueryChange = {},
onDispatchQuery = {},
queryResult = Result.success(items),
selected = items[1],
onSelectedChange = {},
onConfirmSelection = {},
onDismissRequest = {},
)
}
private val fullPageWithoutSelected = @Composable {
MangaTrackerSearch(
query = TextFieldValue(text = ""),
onQueryChange = {},
onDispatchQuery = {},
queryResult = Result.success(someTrackSearches().take(30).toList()),
selected = null,
onSelectedChange = {},
onConfirmSelection = {},
onDismissRequest = {},
)
}
private val loading = @Composable {
MangaTrackerSearch(
query = TextFieldValue(),
onQueryChange = {},
onDispatchQuery = {},
queryResult = null,
selected = null,
onSelectedChange = {},
onConfirmSelection = {},
onDismissRequest = {},
)
}
override val values: Sequence<@Composable () -> Unit> = sequenceOf(
fullPageWithSecondSelected,
fullPageWithoutSelected,
loading,
)
private fun someTrackSearches(): Sequence<MangaTrackSearch> = sequence {
while (true) {
yield(randTrackSearch())
}
}
private fun randTrackSearch() = MangaTrackSearch().let {
it.id = Random.nextLong()
it.manga_id = Random.nextLong()
it.sync_id = Random.nextInt()
it.media_id = Random.nextLong()
it.library_id = Random.nextLong()
it.title = lorem((1..10).random()).joinToString()
it.last_chapter_read = (0..100).random().toFloat()
it.total_chapters = (100..1000).random()
it.score = (0..10).random().toFloat()
it.status = Random.nextInt()
it.started_reading_date = 0L
it.finished_reading_date = 0L
it.tracking_url = "https://example.com/tracker-example"
it.cover_url = "https://example.com/cover.png"
it.start_date = Instant.now().minus((1L..365).random(), ChronoUnit.DAYS).toString()
it.summary = lorem((0..40).random()).joinToString()
it
}
private fun lorem(words: Int): Sequence<String> =
LoremIpsum(words).values
}

View file

@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material.icons.outlined.ArrowForward
import androidx.compose.material.icons.outlined.Close
@ -130,7 +129,7 @@ fun WebViewScreenContent(
),
AppBar.Action(
title = stringResource(R.string.action_webview_forward),
icon = Icons.AutoMirrored.Outlined.ArrowForward,
icon = Icons.Outlined.ArrowForward,
onClick = {
if (navigator.canGoForward) {
navigator.navigateForward()
@ -181,7 +180,7 @@ fun WebViewScreenContent(
.align(Alignment.BottomCenter),
)
is LoadingState.Loading -> LinearProgressIndicator(
progress = { (loadingState as? LoadingState.Loading)?.progress ?: 1f },
progress = (loadingState as? LoadingState.Loading)?.progress ?: 1f,
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter),

View file

@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadManager
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadManager
import eu.kanade.tachiyomi.data.library.anime.AnimeLibraryUpdateJob
import eu.kanade.tachiyomi.data.library.manga.MangaLibraryUpdateJob
import eu.kanade.tachiyomi.data.updater.AppUpdateService
import eu.kanade.tachiyomi.data.updater.AppUpdateDownloadJob
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.player.PlayerActivity
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
@ -108,6 +108,8 @@ class NotificationReceiver : BroadcastReceiver() {
// Cancel library update and dismiss notification
ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context)
ACTION_CANCEL_ANIMELIB_UPDATE -> cancelAnimelibUpdate(context)
// Start downloading app update
ACTION_START_APP_UPDATE -> startDownloadAppUpdate(context, intent)
// Cancel downloading app update
ACTION_CANCEL_APP_UPDATE_DOWNLOAD -> cancelDownloadAppUpdate(context)
// Open reader activity
@ -301,20 +303,24 @@ class NotificationReceiver : BroadcastReceiver() {
MangaLibraryUpdateJob.stop(context)
}
private fun cancelDownloadAppUpdate(context: Context) {
AppUpdateService.stop(context)
}
/**
* Method called when user wants to stop a library update
*
* @param context context of application
* @param notificationId id of notification
*/
private fun cancelAnimelibUpdate(context: Context) {
AnimeLibraryUpdateJob.stop(context)
}
private fun startDownloadAppUpdate(context: Context, intent: Intent) {
val url = intent.getStringExtra(AppUpdateDownloadJob.EXTRA_DOWNLOAD_URL) ?: return
AppUpdateDownloadJob.start(context, url)
}
private fun cancelDownloadAppUpdate(context: Context) {
AppUpdateDownloadJob.stop(context)
}
/**
* Method called when user wants to mark manga chapters as read
*
@ -414,6 +420,7 @@ class NotificationReceiver : BroadcastReceiver() {
private const val ACTION_CANCEL_LIBRARY_UPDATE = "$ID.$NAME.CANCEL_LIBRARY_UPDATE"
private const val ACTION_CANCEL_ANIMELIB_UPDATE = "$ID.$NAME.CANCEL_ANIMELIB_UPDATE"
private const val ACTION_START_APP_UPDATE = "$ID.$NAME.ACTION_START_APP_UPDATE"
private const val ACTION_CANCEL_APP_UPDATE_DOWNLOAD = "$ID.$NAME.CANCEL_APP_UPDATE_DOWNLOAD"
private const val ACTION_MARK_AS_READ = "$ID.$NAME.MARK_AS_READ"
@ -879,10 +886,25 @@ class NotificationReceiver : BroadcastReceiver() {
)
}
/**
* Returns [PendingIntent] that starts the [AppUpdateDownloadJob] to download an app update.
*
* @param context context of application
* @return [PendingIntent]
*/
internal fun downloadAppUpdatePendingBroadcast(context: Context, url: String, title: String? = null): PendingIntent {
return Intent(context, NotificationReceiver::class.java).run {
action = ACTION_START_APP_UPDATE
putExtra(AppUpdateDownloadJob.EXTRA_DOWNLOAD_URL, url)
title?.let { putExtra(AppUpdateDownloadJob.EXTRA_DOWNLOAD_TITLE, it) }
PendingIntent.getBroadcast(context, 0, this, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
}
}
/**
*
*/
internal fun cancelUpdateDownloadPendingBroadcast(context: Context): PendingIntent {
internal fun cancelDownloadAppUpdatePendingBroadcast(context: Context): PendingIntent {
val intent = Intent(context, NotificationReceiver::class.java).apply {
action = ACTION_CANCEL_APP_UPDATE_DOWNLOAD
}

View file

@ -23,6 +23,7 @@ import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.InputStream
import java.util.Date
class ImageSaver(
val context: Context,
@ -79,6 +80,7 @@ class ImageSaver(
MediaStore.Images.Media.RELATIVE_PATH to relativePath,
MediaStore.Images.Media.DISPLAY_NAME to image.name,
MediaStore.Images.Media.MIME_TYPE to type.mime,
MediaStore.Images.Media.DATE_MODIFIED to Date().time * 1000,
)
val picture = findUriOrDefault(relativePath, filename) {

View file

@ -25,7 +25,8 @@ interface Tracker {
@StringRes
fun getStatus(status: Int): Int?
fun getCompletionStatus(): Int
fun getScoreList(): List<String>
suspend fun login(username: String, password: String)
@CallSuper

View file

@ -28,7 +28,7 @@ class TrackerManager(context: Context) {
val bangumi = Bangumi(5L)
val komga = Komga(6L)
val mangaUpdates = MangaUpdates(7L)
val kavita = Kavita(context, KAVITA)
val kavita = Kavita(KAVITA)
val suwayomi = Suwayomi(9L)
val simkl = Simkl(SIMKL)

View file

@ -12,7 +12,6 @@ import eu.kanade.tachiyomi.data.track.DeletableMangaTracker
import eu.kanade.tachiyomi.data.track.MangaTracker
import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch
import eu.kanade.tachiyomi.data.track.model.MangaTrackSearch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import uy.kohesive.injekt.injectLazy
@ -161,19 +160,19 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), AnimeTracker, MangaTracker, De
}
}
override suspend fun bind(track: AnimeTrack, hasReadChapters: Boolean): AnimeTrack {
override suspend fun bind(track: AnimeTrack, hasWatchedEpisodes: Boolean): AnimeTrack {
val remoteTrack = api.findLibAnime(track, getUserId())
return if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack)
track.media_id = remoteTrack.media_id
if (track.status != COMPLETED) {
track.status = if (hasReadChapters) WATCHING else track.status
track.status = if (hasWatchedEpisodes) WATCHING else track.status
}
update(track)
} else {
track.status = if (hasReadChapters) WATCHING else PLAN_TO_WATCH
track.status = if (hasWatchedEpisodes) WATCHING else PLAN_TO_WATCH
track.score = 0F
add(track)
}

View file

@ -0,0 +1,148 @@
package eu.kanade.tachiyomi.data.updater
import android.content.Context
import androidx.work.Constraints
import androidx.work.CoroutineWorker
import androidx.work.ExistingWorkPolicy
import androidx.work.ForegroundInfo
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.ProgressListener
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.newCachelessCallWithProgress
import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.storage.saveTo
import eu.kanade.tachiyomi.util.system.workManager
import logcat.LogPriority
import okhttp3.internal.http2.ErrorCode
import okhttp3.internal.http2.StreamResetException
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.injectLazy
import java.io.File
import kotlin.coroutines.cancellation.CancellationException
class AppUpdateDownloadJob(private val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {
private val notifier = AppUpdateNotifier(context)
private val network: NetworkHelper by injectLazy()
override suspend fun doWork(): Result {
val url = inputData.getString(EXTRA_DOWNLOAD_URL)
val title = inputData.getString(EXTRA_DOWNLOAD_TITLE) ?: context.getString(R.string.app_name)
if (url.isNullOrEmpty()) {
return Result.failure()
}
try {
setForeground(getForegroundInfo())
} catch (e: IllegalStateException) {
logcat(LogPriority.ERROR, e) { "Not allowed to run on foreground service" }
}
withIOContext {
downloadApk(title, url)
}
return Result.success()
}
override suspend fun getForegroundInfo(): ForegroundInfo {
return ForegroundInfo(
Notifications.ID_APP_UPDATER,
notifier.onDownloadStarted().build(),
)
}
/**
* Called to start downloading apk of new update
*
* @param url url location of file
*/
private suspend fun downloadApk(title: String, url: String) {
// Show notification download starting.
notifier.onDownloadStarted(title)
val progressListener = object : ProgressListener {
// Progress of the download
var savedProgress = 0
// Keep track of the last notification sent to avoid posting too many.
var lastTick = 0L
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
val progress = (100 * (bytesRead.toFloat() / contentLength)).toInt()
val currentTime = System.currentTimeMillis()
if (progress > savedProgress && currentTime - 200 > lastTick) {
savedProgress = progress
lastTick = currentTime
notifier.onProgressChange(progress)
}
}
}
try {
// Download the new update.
val response = network.client.newCachelessCallWithProgress(GET(url), progressListener)
.await()
// File where the apk will be saved.
val apkFile = File(context.externalCacheDir, "update.apk")
if (response.isSuccessful) {
response.body.source().saveTo(apkFile)
} else {
response.close()
throw Exception("Unsuccessful response")
}
notifier.cancel()
notifier.promptInstall(apkFile.getUriCompat(context))
} catch (e: Exception) {
val shouldCancel = e is CancellationException ||
(e is StreamResetException && e.errorCode == ErrorCode.CANCEL)
if (shouldCancel) {
notifier.cancel()
} else {
notifier.onDownloadError(url)
}
}
}
companion object {
private const val TAG = "AppUpdateDownload"
const val EXTRA_DOWNLOAD_URL = "DOWNLOAD_URL"
const val EXTRA_DOWNLOAD_TITLE = "DOWNLOAD_TITLE"
fun start(context: Context, url: String, title: String? = null) {
val constraints = Constraints(
requiredNetworkType = NetworkType.CONNECTED,
)
val request = OneTimeWorkRequestBuilder<AppUpdateDownloadJob>()
.setConstraints(constraints)
.addTag(TAG)
.setInputData(
workDataOf(
EXTRA_DOWNLOAD_URL to url,
EXTRA_DOWNLOAD_TITLE to title,
),
)
.build()
context.workManager.enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, request)
}
fun stop(context: Context) {
context.workManager.cancelUniqueWork(TAG)
}
}
}

View file

@ -34,16 +34,11 @@ internal class AppUpdateNotifier(private val context: Context) {
@SuppressLint("LaunchActivityFromNotification")
fun promptUpdate(release: Release) {
val updateIntent = Intent(context, AppUpdateService::class.java).run {
putExtra(AppUpdateService.EXTRA_DOWNLOAD_URL, release.getDownloadLink())
putExtra(AppUpdateService.EXTRA_DOWNLOAD_TITLE, release.version)
PendingIntent.getService(
context,
0,
this,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
}
val updateIntent = NotificationReceiver.downloadAppUpdatePendingBroadcast(
context,
release.getDownloadLink(),
release.version,
)
val releaseIntent = Intent(Intent.ACTION_VIEW, release.releaseLink.toUri()).run {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
@ -94,7 +89,7 @@ internal class AppUpdateNotifier(private val context: Context) {
addAction(
R.drawable.ic_close_24dp,
context.getString(R.string.action_cancel),
NotificationReceiver.cancelUpdateDownloadPendingBroadcast(context),
NotificationReceiver.cancelDownloadAppUpdatePendingBroadcast(context),
)
}
notificationBuilder.show()
@ -184,7 +179,7 @@ internal class AppUpdateNotifier(private val context: Context) {
addAction(
R.drawable.ic_refresh_24dp,
context.getString(R.string.action_retry),
AppUpdateService.downloadApkPendingService(context, url),
NotificationReceiver.downloadAppUpdatePendingBroadcast(context, url),
)
addAction(
R.drawable.ic_close_24dp,

View file

@ -1,204 +0,0 @@
package eu.kanade.tachiyomi.data.updater
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.IBinder
import android.os.PowerManager
import androidx.core.content.ContextCompat
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.ProgressListener
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.newCachelessCallWithProgress
import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.storage.saveTo
import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.isServiceRunning
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import okhttp3.internal.http2.ErrorCode
import okhttp3.internal.http2.StreamResetException
import uy.kohesive.injekt.injectLazy
import java.io.File
class AppUpdateService : Service() {
private val network: NetworkHelper by injectLazy()
/**
* Wake lock that will be held until the service is destroyed.
*/
private lateinit var wakeLock: PowerManager.WakeLock
private lateinit var notifier: AppUpdateNotifier
private val job = SupervisorJob()
private val serviceScope = CoroutineScope(Dispatchers.IO + job)
override fun onCreate() {
notifier = AppUpdateNotifier(this)
wakeLock = acquireWakeLock(javaClass.name)
startForeground(Notifications.ID_APP_UPDATER, notifier.onDownloadStarted().build())
}
/**
* This method needs to be implemented, but it's not used/needed.
*/
override fun onBind(intent: Intent): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent == null) return START_NOT_STICKY
val url = intent.getStringExtra(EXTRA_DOWNLOAD_URL) ?: return START_NOT_STICKY
val title = intent.getStringExtra(EXTRA_DOWNLOAD_TITLE) ?: getString(R.string.app_name)
serviceScope.launch {
downloadApk(title, url)
}
job.invokeOnCompletion { stopSelf(startId) }
return START_NOT_STICKY
}
override fun stopService(name: Intent?): Boolean {
destroyJob()
return super.stopService(name)
}
override fun onDestroy() {
destroyJob()
}
private fun destroyJob() {
serviceScope.cancel()
job.cancel()
if (wakeLock.isHeld) {
wakeLock.release()
}
}
/**
* Called to start downloading apk of new update
*
* @param url url location of file
*/
private suspend fun downloadApk(title: String, url: String) {
// Show notification download starting.
notifier.onDownloadStarted(title)
val progressListener = object : ProgressListener {
// Progress of the download
var savedProgress = 0
// Keep track of the last notification sent to avoid posting too many.
var lastTick = 0L
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
val progress = (100 * (bytesRead.toFloat() / contentLength)).toInt()
val currentTime = System.currentTimeMillis()
if (progress > savedProgress && currentTime - 200 > lastTick) {
savedProgress = progress
lastTick = currentTime
notifier.onProgressChange(progress)
}
}
}
try {
// Download the new update.
val response = network.client.newCachelessCallWithProgress(GET(url), progressListener)
.await()
// File where the apk will be saved.
val apkFile = File(externalCacheDir, "update.apk")
if (response.isSuccessful) {
response.body.source().saveTo(apkFile)
} else {
response.close()
throw Exception("Unsuccessful response")
}
notifier.promptInstall(apkFile.getUriCompat(this))
} catch (e: Exception) {
val shouldCancel = e is CancellationException ||
(e is StreamResetException && e.errorCode == ErrorCode.CANCEL)
if (shouldCancel) {
notifier.cancel()
} else {
notifier.onDownloadError(url)
}
}
}
companion object {
internal const val EXTRA_DOWNLOAD_URL = "${BuildConfig.APPLICATION_ID}.UpdaterService.DOWNLOAD_URL"
internal const val EXTRA_DOWNLOAD_TITLE = "${BuildConfig.APPLICATION_ID}.UpdaterService.DOWNLOAD_TITLE"
/**
* Returns the status of the service.
*
* @param context the application context.
* @return true if the service is running, false otherwise.
*/
private fun isRunning(context: Context): Boolean =
context.isServiceRunning(AppUpdateService::class.java)
/**
* Downloads a new update and let the user install the new version from a notification.
*
* @param context the application context.
* @param url the url to the new update.
*/
fun start(
context: Context,
url: String,
title: String? = context.getString(R.string.app_name),
) {
if (isRunning(context)) return
Intent(context, AppUpdateService::class.java).apply {
putExtra(EXTRA_DOWNLOAD_TITLE, title)
putExtra(EXTRA_DOWNLOAD_URL, url)
ContextCompat.startForegroundService(context, this)
}
}
/**
* Stops the service.
*
* @param context the application context
*/
fun stop(context: Context) {
context.stopService(Intent(context, AppUpdateService::class.java))
}
/**
* Returns [PendingIntent] that starts a service which downloads the apk specified in url.
*
* @param url the url to the new update.
* @return [PendingIntent]
*/
internal fun downloadApkPendingService(context: Context, url: String): PendingIntent {
return Intent(context, AppUpdateService::class.java).run {
putExtra(EXTRA_DOWNLOAD_URL, url)
PendingIntent.getService(
context,
0,
this,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
}
}
}
}

View file

@ -0,0 +1,58 @@
package eu.kanade.tachiyomi.dev.preview
import android.graphics.Color
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch
import eu.kanade.tachiyomi.data.track.model.MangaTrackSearch
import okhttp3.OkHttpClient
import tachiyomi.domain.track.manga.model.MangaTrack
data class DummyTracker(
override val id: Long,
override val name: String,
override val supportsReadingDates: Boolean = false,
override val isLoggedIn: Boolean = false,
val valLogoColor: Int = Color.rgb(18, 25, 35),
val valLogo: Int = R.drawable.ic_tracker_anilist,
val valStatuses: List<Int> = (1..6).toList(),
val valCompletionStatus: Int = 2,
val valScoreList: List<String> = (0..10).map(Int::toString),
val val10PointScore: Double = 5.4,
val valMangaSearchResults: List<MangaTrackSearch> = listOf(),
val valAnimeSearchResults: List<AnimeTrackSearch> = listOf(),
) : Tracker {
override val client: OkHttpClient
get() = TODO("Not yet implemented")
override fun getLogoColor(): Int = valLogoColor
override fun getLogo(): Int = valLogo
override fun getStatus(status: Int): Int? = when (status) {
1 -> R.string.reading
2 -> R.string.plan_to_read
3 -> R.string.completed
4 -> R.string.on_hold
5 -> R.string.dropped
6 -> R.string.repeating
7 -> R.string.watching
8 -> R.string.plan_to_watch
else -> null
}
override fun getCompletionStatus(): Int = valCompletionStatus
override fun getScoreList(): List<String> = valScoreList
override suspend fun login(username: String, password: String) = Unit
override fun logout() = Unit
override fun getUsername(): String = "username"
override fun getPassword(): String = "passw0rd"
override fun saveCredentials(username: String, password: String) = Unit
}

View file

@ -14,7 +14,7 @@ import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
import cafe.adriel.voyager.navigator.tab.TabOptions
import eu.kanade.presentation.components.TabbedScreen
import eu.kanade.presentation.extensions.RequestStoragePermission
import eu.kanade.presentation.permissions.PermissionRequestHelper
import eu.kanade.presentation.util.Tab
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.anime.extension.AnimeExtensionsScreenModel
@ -27,7 +27,6 @@ import eu.kanade.tachiyomi.ui.browse.manga.extension.mangaExtensionsTab
import eu.kanade.tachiyomi.ui.browse.manga.migration.sources.migrateMangaSourceTab
import eu.kanade.tachiyomi.ui.browse.manga.source.mangaSourcesTab
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.storage.DiskUtil
data class BrowseTab(
private val toExtensions: Boolean = false,
@ -80,7 +79,7 @@ data class BrowseTab(
)
// For local source
DiskUtil.RequestStoragePermission()
PermissionRequestHelper.requestStoragePermission()
LaunchedEffect(Unit) {
(context as? MainActivity)?.ready = true

View file

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.ui.browse.anime.migration.sources
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
@ -30,7 +29,7 @@ fun Screen.migrateAnimeSourceTab(): TabContent {
actions = listOf(
AppBar.Action(
title = stringResource(R.string.migration_help_guide),
icon = Icons.AutoMirrored.Outlined.HelpOutline,
icon = Icons.Outlined.HelpOutline,
onClick = {
uriHandler.openUri("https://aniyomi.org/help/guides/source-migration/")
},

View file

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.ui.browse.manga.migration.sources
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
@ -30,7 +29,7 @@ fun Screen.migrateMangaSourceTab(): TabContent {
actions = listOf(
AppBar.Action(
title = stringResource(R.string.migration_help_guide),
icon = Icons.AutoMirrored.Outlined.HelpOutline,
icon = Icons.Outlined.HelpOutline,
onClick = {
uriHandler.openUri("https://aniyomi.org/help/guides/source-migration/")
},

View file

@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
@ -14,6 +15,7 @@ import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.anime.source.globalsearch.GlobalAnimeSearchScreen
import eu.kanade.tachiyomi.ui.entries.anime.AnimeScreen
import eu.kanade.tachiyomi.ui.player.PlayerActivity
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.screens.LoadingScreen
@ -23,6 +25,7 @@ class DeepLinkAnimeScreen(
@Composable
override fun Content() {
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel {
@ -46,12 +49,22 @@ class DeepLinkAnimeScreen(
navigator.replace(GlobalAnimeSearchScreen(query))
}
is DeepLinkAnimeScreenModel.State.Result -> {
navigator.replace(
AnimeScreen(
(state as DeepLinkAnimeScreenModel.State.Result).anime.id,
true,
),
)
val resultState = state as DeepLinkAnimeScreenModel.State.Result
if (resultState.episodeId == null) {
navigator.replace(
AnimeScreen(
resultState.anime.id,
true,
),
)
} else {
navigator.pop()
PlayerActivity.newIntent(
context,
resultState.anime.id,
resultState.episodeId,
).also(context::startActivity)
}
}
}
}

View file

@ -4,10 +4,20 @@ import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope
import eu.kanade.domain.entries.anime.model.toDomainAnime
import eu.kanade.domain.entries.anime.model.toSAnime
import eu.kanade.domain.items.episode.interactor.SyncEpisodesWithSource
import eu.kanade.tachiyomi.animesource.AnimeSource
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.online.ResolvableAnimeSource
import eu.kanade.tachiyomi.animesource.online.UriType
import kotlinx.coroutines.flow.update
import tachiyomi.core.util.lang.launchIO
import tachiyomi.domain.entries.anime.interactor.NetworkToLocalAnime
import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.domain.entries.manga.interactor.GetAnimeByUrlAndSourceId
import tachiyomi.domain.items.episode.interactor.GetEpisodeByUrlAndAnimeId
import tachiyomi.domain.items.episode.model.Episode
import tachiyomi.domain.source.anime.service.AnimeSourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -15,25 +25,59 @@ import uy.kohesive.injekt.api.get
class DeepLinkAnimeScreenModel(
query: String = "",
private val sourceManager: AnimeSourceManager = Injekt.get(),
private val networkToLocalAnime: NetworkToLocalAnime = Injekt.get(),
private val getEpisodeByUrlAndAnimeId: GetEpisodeByUrlAndAnimeId = Injekt.get(),
private val getAnimeByUrlAndSourceId: GetAnimeByUrlAndSourceId = Injekt.get(),
private val syncEpisodesWithSource: SyncEpisodesWithSource = Injekt.get(),
) : StateScreenModel<DeepLinkAnimeScreenModel.State>(State.Loading) {
init {
coroutineScope.launchIO {
val anime = sourceManager.getCatalogueSources()
val source = sourceManager.getCatalogueSources()
.filterIsInstance<ResolvableAnimeSource>()
.filter { it.canResolveUri(query) }
.firstNotNullOfOrNull { it.getAnime(query)?.toDomainAnime(it.id) }
.firstOrNull { it.getUriType(query) != UriType.Unknown }
val anime = source?.getAnime(query)?.let {
getAnimeFromSAnime(it, source.id)
}
val episode = if (source?.getUriType(query) == UriType.Episode && anime != null) {
source.getEpisode(query)?.let { getEpisodeFromSEpisode(it, anime, source) }
} else {
null
}
mutableState.update {
if (anime == null) {
State.NoResults
} else {
State.Result(anime)
if (episode == null) {
State.Result(anime)
} else {
State.Result(anime, episode.id)
}
}
}
}
}
private suspend fun getEpisodeFromSEpisode(sEpisode: SEpisode, anime: Anime, source: AnimeSource): Episode? {
val localEpisode = getEpisodeByUrlAndAnimeId.await(sEpisode.url, anime.id)
return if (localEpisode == null) {
val sourceEpisodes = source.getEpisodeList(anime.toSAnime())
val newEpisodes = syncEpisodesWithSource.await(sourceEpisodes, anime, source, false)
newEpisodes.find { it.url == sEpisode.url }
} else {
localEpisode
}
}
private suspend fun getAnimeFromSAnime(sAnime: SAnime, sourceId: Long): Anime {
return getAnimeByUrlAndSourceId.awaitAnime(sAnime.url, sourceId)
?: networkToLocalAnime.await(sAnime.toDomainAnime(sourceId))
}
sealed interface State {
@Immutable
data object Loading : State
@ -42,6 +86,6 @@ class DeepLinkAnimeScreenModel(
data object NoResults : State
@Immutable
data class Result(val anime: Anime) : State
data class Result(val anime: Anime, val episodeId: Long? = null) : State
}
}

View file

@ -5,7 +5,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.core.content.ContextCompat.startActivity
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
@ -14,6 +16,7 @@ import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.manga.source.globalsearch.GlobalMangaSearchScreen
import eu.kanade.tachiyomi.ui.entries.manga.MangaScreen
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.screens.LoadingScreen
@ -23,6 +26,7 @@ class DeepLinkMangaScreen(
@Composable
override fun Content() {
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel {
@ -46,12 +50,22 @@ class DeepLinkMangaScreen(
navigator.replace(GlobalMangaSearchScreen(query))
}
is DeepLinkMangaScreenModel.State.Result -> {
navigator.replace(
MangaScreen(
(state as DeepLinkMangaScreenModel.State.Result).manga.id,
true,
),
)
val resultState = state as DeepLinkMangaScreenModel.State.Result
if (resultState.chapterId == null) {
navigator.replace(
MangaScreen(
resultState.manga.id,
true,
),
)
} else {
navigator.pop()
ReaderActivity.newIntent(
context,
resultState.manga.id,
resultState.chapterId,
).also(context::startActivity)
}
}
}
}

View file

@ -4,10 +4,20 @@ import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope
import eu.kanade.domain.entries.manga.model.toDomainManga
import eu.kanade.domain.entries.manga.model.toSManga
import eu.kanade.domain.items.chapter.interactor.SyncChaptersWithSource
import eu.kanade.tachiyomi.source.MangaSource
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ResolvableMangaSource
import eu.kanade.tachiyomi.source.online.UriType
import kotlinx.coroutines.flow.update
import tachiyomi.core.util.lang.launchIO
import tachiyomi.domain.entries.anime.interactor.GetMangaByUrlAndSourceId
import tachiyomi.domain.entries.manga.interactor.NetworkToLocalManga
import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.items.chapter.interactor.GetChapterByUrlAndMangaId
import tachiyomi.domain.items.chapter.model.Chapter
import tachiyomi.domain.source.manga.service.MangaSourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -15,25 +25,59 @@ import uy.kohesive.injekt.api.get
class DeepLinkMangaScreenModel(
query: String = "",
private val sourceManager: MangaSourceManager = Injekt.get(),
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
private val getChapterByUrlAndMangaId: GetChapterByUrlAndMangaId = Injekt.get(),
private val getMangaByUrlAndSourceId: GetMangaByUrlAndSourceId = Injekt.get(),
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(),
) : StateScreenModel<DeepLinkMangaScreenModel.State>(State.Loading) {
init {
coroutineScope.launchIO {
val manga = sourceManager.getCatalogueSources()
val source = sourceManager.getCatalogueSources()
.filterIsInstance<ResolvableMangaSource>()
.filter { it.canResolveUri(query) }
.firstNotNullOfOrNull { it.getManga(query)?.toDomainManga(it.id) }
.firstOrNull { it.getUriType(query) != UriType.Unknown }
val manga = source?.getManga(query)?.let {
getMangaFromSManga(it, source.id)
}
val chapter = if (source?.getUriType(query) == UriType.Chapter && manga != null) {
source.getChapter(query)?.let { getChapterFromSChapter(it, manga, source) }
} else {
null
}
mutableState.update {
if (manga == null) {
State.NoResults
} else {
State.Result(manga)
if (chapter == null) {
State.Result(manga)
} else {
State.Result(manga, chapter.id)
}
}
}
}
}
private suspend fun getChapterFromSChapter(sChapter: SChapter, manga: Manga, source: MangaSource): Chapter? {
val localChapter = getChapterByUrlAndMangaId.await(sChapter.url, manga.id)
return if (localChapter == null) {
val sourceChapters = source.getChapterList(manga.toSManga())
val newChapters = syncChaptersWithSource.await(sourceChapters, manga, source, false)
newChapters.find { it.url == sChapter.url }
} else {
localChapter
}
}
private suspend fun getMangaFromSManga(sManga: SManga, sourceId: Long): Manga {
return getMangaByUrlAndSourceId.awaitManga(sManga.url, sourceId)
?: networkToLocalManga.await(sManga.toDomainManga(sourceId))
}
sealed interface State {
@Immutable
data object Loading : State
@ -42,6 +86,6 @@ class DeepLinkMangaScreenModel(
data object NoResults : State
@Immutable
data class Result(val manga: Manga) : State
data class Result(val manga: Manga, val chapterId: Long? = null) : State
}
}

View file

@ -10,13 +10,12 @@ import androidx.compose.ui.res.stringResource
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
import cafe.adriel.voyager.navigator.tab.TabOptions
import eu.kanade.presentation.components.TabbedScreen
import eu.kanade.presentation.extensions.RequestStoragePermission
import eu.kanade.presentation.permissions.PermissionRequestHelper
import eu.kanade.presentation.util.Tab
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.download.anime.animeDownloadTab
import eu.kanade.tachiyomi.ui.download.manga.mangaDownloadTab
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.storage.DiskUtil
data class DownloadsTab(
private val isManga: Boolean = false,
@ -51,6 +50,6 @@ data class DownloadsTab(
}
// For local source
DiskUtil.RequestStoragePermission()
PermissionRequestHelper.requestStoragePermission()
}
}

View file

@ -15,7 +15,7 @@ import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
import cafe.adriel.voyager.navigator.tab.TabOptions
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.components.TabbedScreen
import eu.kanade.presentation.extensions.RequestStoragePermission
import eu.kanade.presentation.permissions.PermissionRequestHelper
import eu.kanade.presentation.util.Tab
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.history.anime.AnimeHistoryScreenModel
@ -24,7 +24,6 @@ import eu.kanade.tachiyomi.ui.history.anime.resumeLastEpisodeSeenEvent
import eu.kanade.tachiyomi.ui.history.manga.MangaHistoryScreenModel
import eu.kanade.tachiyomi.ui.history.manga.mangaHistoryTab
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.storage.DiskUtil
data class HistoriesTab(
private val fromMore: Boolean,
@ -75,7 +74,7 @@ data class HistoriesTab(
}
// For local source
DiskUtil.RequestStoragePermission()
PermissionRequestHelper.requestStoragePermission()
}
}

View file

@ -195,7 +195,7 @@ object AnimeLibraryTab : Tab() {
actions = listOf(
EmptyScreenAction(
stringResId = R.string.getting_started_guide,
icon = Icons.AutoMirrored.Outlined.HelpOutline,
icon = Icons.Outlined.HelpOutline,
onClick = {
handler.openUri(
"https://aniyomi.org/docs/guides/getting-started",

View file

@ -7,7 +7,6 @@ import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
import androidx.compose.animation.graphics.vector.AnimatedImageVector
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SnackbarHost
@ -191,7 +190,7 @@ object MangaLibraryTab : Tab() {
actions = listOf(
EmptyScreenAction(
stringResId = R.string.getting_started_guide,
icon = Icons.AutoMirrored.Outlined.HelpOutline,
icon = Icons.Outlined.HelpOutline,
onClick = {
handler.openUri(
"https://aniyomi.org/docs/guides/getting-started",

View file

@ -81,7 +81,7 @@ object MoreTab : Tab() {
onClickCategories = { navigator.push(CategoriesTab()) },
onClickStats = { navigator.push(StatsTab()) },
onClickStorage = { navigator.push(StorageTab()) },
onClickBackupAndRestore = { navigator.push(SettingsScreen.toBackupScreen()) },
onClickDataAndStorage = { navigator.push(SettingsScreen.toDataAndStorageScreen()) },
onClickSettings = { navigator.push(SettingsScreen.toMainScreen()) },
onClickAbout = { navigator.push(SettingsScreen.toAboutScreen()) },
)

View file

@ -7,7 +7,7 @@ import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.more.NewUpdateScreen
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.data.updater.AppUpdateService
import eu.kanade.tachiyomi.data.updater.AppUpdateDownloadJob
import eu.kanade.tachiyomi.util.system.openInBrowser
class NewUpdateScreen(
@ -31,7 +31,7 @@ class NewUpdateScreen(
onOpenInBrowser = { context.openInBrowser(releaseLink) },
onRejectUpdate = navigator::pop,
onAcceptUpdate = {
AppUpdateService.start(
AppUpdateDownloadJob.start(
context = context,
url = downloadLink,
title = versionName,

View file

@ -41,6 +41,7 @@ import com.google.android.material.elevation.SurfaceColors
import com.google.android.material.transition.platform.MaterialContainerTransform
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.domain.base.BasePreferences
import eu.kanade.presentation.reader.BrightnessOverlay
import eu.kanade.presentation.reader.OrientationModeSelectDialog
import eu.kanade.presentation.reader.PageIndicatorText
import eu.kanade.presentation.reader.ReaderPageActionsDialog
@ -304,12 +305,6 @@ class ReaderActivity : BaseActivity() {
* Initializes the reader menu. It sets up click listeners and the initial visibility.
*/
private fun initializeMenu() {
binding.dialogRoot.applyInsetter {
type(navigationBars = true) {
margin(vertical = true, horizontal = true)
}
}
binding.pageNumber.setComposeContent {
val state by viewModel.state.collectAsState()
val showPageNumber by viewModel.readerPreferences.showPageNumber().collectAsState()
@ -384,6 +379,10 @@ class ReaderActivity : BaseActivity() {
onClickSettings = viewModel::openSettingsDialog,
)
BrightnessOverlay(
value = state.brightnessOverlayValue,
)
val onDismissRequest = viewModel::closeDialog
when (state.dialog) {
is ReaderViewModel.Dialog.Loading -> {
@ -918,17 +917,9 @@ class ReaderActivity : BaseActivity() {
}
else -> WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
}
window.attributes = window.attributes.apply { screenBrightness = readerBrightness }
// Set black overlay visibility.
if (value < 0) {
binding.brightnessOverlay.isVisible = true
val alpha = (abs(value) * 2.56).toInt()
binding.brightnessOverlay.setBackgroundColor(Color.argb(alpha, 0, 0, 0))
} else {
binding.brightnessOverlay.isVisible = false
}
viewModel.setBrightnessOverlayValue(value)
}
/**

View file

@ -761,6 +761,10 @@ class ReaderViewModel @JvmOverloads constructor(
mutableState.update { it.copy(dialog = null) }
}
fun setBrightnessOverlayValue(value: Int) {
mutableState.update { it.copy(brightnessOverlayValue = value) }
}
/**
* Saves the image of this the selected page on the pictures directory and notifies the UI of the result.
* There's also a notification to allow sharing the image somewhere else or deleting it.
@ -922,6 +926,7 @@ class ReaderViewModel @JvmOverloads constructor(
val viewer: Viewer? = null,
val dialog: Dialog? = null,
val menuVisible: Boolean = false,
val brightnessOverlayValue: Int = 0,
) {
val currentChapter: ReaderChapter?
get() = viewerChapters?.currChapter

View file

@ -13,7 +13,7 @@ import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.more.settings.screen.SettingsAppearanceScreen
import eu.kanade.presentation.more.settings.screen.SettingsBackupScreen
import eu.kanade.presentation.more.settings.screen.SettingsDataScreen
import eu.kanade.presentation.more.settings.screen.SettingsMainScreen
import eu.kanade.presentation.more.settings.screen.about.AboutScreen
import eu.kanade.presentation.util.DefaultNavigatorScreenTransition
@ -23,7 +23,7 @@ import eu.kanade.presentation.util.isTabletUi
import tachiyomi.presentation.core.components.TwoPanelBox
class SettingsScreen private constructor(
val toBackup: Boolean,
val toDataAndStorage: Boolean,
val toAbout: Boolean,
) : Screen() {
@ -32,8 +32,8 @@ class SettingsScreen private constructor(
val parentNavigator = LocalNavigator.currentOrThrow
if (!isTabletUi()) {
Navigator(
screen = if (toBackup) {
SettingsBackupScreen
screen = if (toDataAndStorage) {
SettingsDataScreen
} else if (toAbout) {
AboutScreen
} else {
@ -54,8 +54,8 @@ class SettingsScreen private constructor(
)
} else {
Navigator(
screen = if (toBackup) {
SettingsBackupScreen
screen = if (toDataAndStorage) {
SettingsDataScreen
} else if (toAbout) {
AboutScreen
} else {
@ -79,10 +79,10 @@ class SettingsScreen private constructor(
}
companion object {
fun toMainScreen() = SettingsScreen(toBackup = false, toAbout = false)
fun toMainScreen() = SettingsScreen(toDataAndStorage = false, toAbout = false)
fun toBackupScreen() = SettingsScreen(toBackup = true, toAbout = false)
fun toDataAndStorageScreen() = SettingsScreen(toDataAndStorage = true, toAbout = false)
fun toAboutScreen() = SettingsScreen(toBackup = false, toAbout = true)
fun toAboutScreen() = SettingsScreen(toDataAndStorage = false, toAbout = true)
}
}

View file

@ -3,6 +3,8 @@ package eu.kanade.tachiyomi.util
import android.content.Context
import android.os.Build
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
@ -10,14 +12,24 @@ import eu.kanade.tachiyomi.util.system.toShareIntent
import eu.kanade.tachiyomi.util.system.toast
import tachiyomi.core.util.lang.withNonCancellableContext
import tachiyomi.core.util.lang.withUIContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class CrashLogUtil(private val context: Context) {
class CrashLogUtil(
private val context: Context,
private val mangaExtensionManager: MangaExtensionManager = Injekt.get(),
private val animeExtensionManager: AnimeExtensionManager = Injekt.get(),
) {
suspend fun dumpLogs() = withNonCancellableContext {
try {
val file = context.createFileInCacheDir("aniyomi_crash_logs.txt")
file.appendText(getDebugInfo() + "\n\n")
getMangaExtensionsInfo()?.let { file.appendText("$it\n\n") }
getAnimeExtensionsInfo()?.let { file.appendText("$it\n\n") }
Runtime.getRuntime().exec("logcat *:E -d -f ${file.absolutePath}").waitFor()
file.appendText(getDebugInfo())
val uri = file.getUriCompat(context)
context.startActivity(uri.toShareIntent(context, "text/plain"))
@ -28,15 +40,65 @@ class CrashLogUtil(private val context: Context) {
fun getDebugInfo(): String {
return """
App version: ${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}, ${BuildConfig.COMMIT_SHA}, ${BuildConfig.VERSION_CODE}, ${BuildConfig.BUILD_TIME})
Android version: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT})
Android version: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT}; build ${Build.DISPLAY})
Android build ID: ${Build.DISPLAY}
Device brand: ${Build.BRAND}
Device manufacturer: ${Build.MANUFACTURER}
Device name: ${Build.DEVICE}
Device name: ${Build.DEVICE} (${Build.PRODUCT})
Device model: ${Build.MODEL}
Device product name: ${Build.PRODUCT}
WebView: ${WebViewUtil.getVersion(context)}
""".trimIndent()
}
private fun getMangaExtensionsInfo(): String? {
val availableExtensions = mangaExtensionManager.availableExtensionsFlow.value.associateBy { it.pkgName }
val extensionInfoList = mangaExtensionManager.installedExtensionsFlow.value
.sortedBy { it.name }
.mapNotNull {
val availableExtension = availableExtensions[it.pkgName]
val hasUpdate = (availableExtension?.versionCode ?: 0) > it.versionCode
if (!hasUpdate && !it.isObsolete && !it.isUnofficial) return@mapNotNull null
"""
- ${it.name}
Installed: ${it.versionName} / Available: ${availableExtension?.versionName ?: "?"}
Obsolete: ${it.isObsolete} / Unofficial: ${it.isUnofficial}
""".trimIndent()
}
return if (extensionInfoList.isNotEmpty()) {
(listOf("Problematic extensions:") + extensionInfoList)
.joinToString("\n")
} else {
null
}
}
private fun getAnimeExtensionsInfo(): String? {
val availableExtensions = animeExtensionManager.availableExtensionsFlow.value.associateBy { it.pkgName }
val extensionInfoList = animeExtensionManager.installedExtensionsFlow.value
.sortedBy { it.name }
.mapNotNull {
val availableExtension = availableExtensions[it.pkgName]
val hasUpdate = (availableExtension?.versionCode ?: 0) > it.versionCode
if (!hasUpdate && !it.isObsolete && !it.isUnofficial) return@mapNotNull null
"""
- ${it.name}
Installed: ${it.versionName} / Available: ${availableExtension?.versionName ?: "?"}
Obsolete: ${it.isObsolete} / Unofficial: ${it.isUnofficial}
""".trimIndent()
}
return if (extensionInfoList.isNotEmpty()) {
(listOf("Problematic extensions:") + extensionInfoList)
.joinToString("\n")
} else {
null
}
}
}

View file

@ -36,12 +36,6 @@
android:focusable="false"
android:visibility="gone" />
<View
android:id="@+id/brightness_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/dialog_root"
android:layout_width="match_parent"

View file

@ -19,7 +19,7 @@ class NetworkPreferences(
fun defaultUserAgent(): Preference<String> {
return preferenceStore.getString(
"default_user_agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/118.0",
)
}
}

View file

@ -1,14 +0,0 @@
package tachiyomi.data.category
import tachiyomi.domain.category.model.Category
val categoryMapper: (Long, String, Long, Long, Long) -> Category =
{ id, name, order, flags, hidden ->
Category(
id = id,
name = name,
order = order,
flags = flags,
hidden = hidden == 1L,
)
}

View file

@ -1,7 +1,6 @@
package tachiyomi.data.category.anime
import kotlinx.coroutines.flow.Flow
import tachiyomi.data.category.categoryMapper
import tachiyomi.data.handlers.anime.AnimeDatabaseHandler
import tachiyomi.domain.category.anime.repository.AnimeCategoryRepository
import tachiyomi.domain.category.model.Category
@ -13,46 +12,46 @@ class AnimeCategoryRepositoryImpl(
) : AnimeCategoryRepository {
override suspend fun getAnimeCategory(id: Long): Category? {
return handler.awaitOneOrNull { categoriesQueries.getCategory(id, categoryMapper) }
return handler.awaitOneOrNull { categoriesQueries.getCategory(id, ::mapCategory) }
}
override suspend fun getAllAnimeCategories(): List<Category> {
return handler.awaitList { categoriesQueries.getCategories(categoryMapper) }
return handler.awaitList { categoriesQueries.getCategories(::mapCategory) }
}
override suspend fun getAllVisibleAnimeCategories(): List<Category> {
return handler.awaitList { categoriesQueries.getVisibleCategories(categoryMapper) }
return handler.awaitList { categoriesQueries.getVisibleCategories(::mapCategory) }
}
override fun getAllAnimeCategoriesAsFlow(): Flow<List<Category>> {
return handler.subscribeToList { categoriesQueries.getCategories(categoryMapper) }
return handler.subscribeToList { categoriesQueries.getCategories(::mapCategory) }
}
override fun getAllVisibleAnimeCategoriesAsFlow(): Flow<List<Category>> {
return handler.subscribeToList { categoriesQueries.getVisibleCategories(categoryMapper) }
return handler.subscribeToList { categoriesQueries.getVisibleCategories(::mapCategory) }
}
override suspend fun getCategoriesByAnimeId(animeId: Long): List<Category> {
return handler.awaitList {
categoriesQueries.getCategoriesByAnimeId(animeId, categoryMapper)
categoriesQueries.getCategoriesByAnimeId(animeId, ::mapCategory)
}
}
override suspend fun getVisibleCategoriesByAnimeId(animeId: Long): List<Category> {
return handler.awaitList {
categoriesQueries.getVisibleCategoriesByAnimeId(animeId, categoryMapper)
categoriesQueries.getVisibleCategoriesByAnimeId(animeId, ::mapCategory)
}
}
override fun getCategoriesByAnimeIdAsFlow(animeId: Long): Flow<List<Category>> {
return handler.subscribeToList {
categoriesQueries.getCategoriesByAnimeId(animeId, categoryMapper)
categoriesQueries.getCategoriesByAnimeId(animeId, ::mapCategory)
}
}
override fun getVisibleCategoriesByAnimeIdAsFlow(animeId: Long): Flow<List<Category>> {
return handler.subscribeToList {
categoriesQueries.getVisibleCategoriesByAnimeId(animeId, categoryMapper)
categoriesQueries.getVisibleCategoriesByAnimeId(animeId, ::mapCategory)
}
}
@ -103,4 +102,20 @@ class AnimeCategoryRepositoryImpl(
)
}
}
private fun mapCategory(
id: Long,
name: String,
order: Long,
flags: Long,
hidden: Long,
): Category {
return Category(
id = id,
name = name,
order = order,
flags = flags,
hidden = hidden == 1L
)
}
}

View file

@ -2,7 +2,6 @@ package tachiyomi.data.category.manga
import kotlinx.coroutines.flow.Flow
import tachiyomi.data.Database
import tachiyomi.data.category.categoryMapper
import tachiyomi.data.handlers.manga.MangaDatabaseHandler
import tachiyomi.domain.category.manga.repository.MangaCategoryRepository
import tachiyomi.domain.category.model.Category
@ -13,46 +12,46 @@ class MangaCategoryRepositoryImpl(
) : MangaCategoryRepository {
override suspend fun getMangaCategory(id: Long): Category? {
return handler.awaitOneOrNull { categoriesQueries.getCategory(id, categoryMapper) }
return handler.awaitOneOrNull { categoriesQueries.getCategory(id, ::mapCategory) }
}
override suspend fun getAllMangaCategories(): List<Category> {
return handler.awaitList { categoriesQueries.getCategories(categoryMapper) }
return handler.awaitList { categoriesQueries.getCategories(::mapCategory) }
}
override suspend fun getAllVisibleMangaCategories(): List<Category> {
return handler.awaitList { categoriesQueries.getVisibleCategories(categoryMapper) }
return handler.awaitList { categoriesQueries.getVisibleCategories(::mapCategory) }
}
override fun getAllMangaCategoriesAsFlow(): Flow<List<Category>> {
return handler.subscribeToList { categoriesQueries.getCategories(categoryMapper) }
return handler.subscribeToList { categoriesQueries.getCategories(::mapCategory) }
}
override fun getAllVisibleMangaCategoriesAsFlow(): Flow<List<Category>> {
return handler.subscribeToList { categoriesQueries.getVisibleCategories(categoryMapper) }
return handler.subscribeToList { categoriesQueries.getVisibleCategories(::mapCategory) }
}
override suspend fun getCategoriesByMangaId(mangaId: Long): List<Category> {
return handler.awaitList {
categoriesQueries.getCategoriesByMangaId(mangaId, categoryMapper)
categoriesQueries.getCategoriesByMangaId(mangaId, ::mapCategory)
}
}
override suspend fun getVisibleCategoriesByMangaId(mangaId: Long): List<Category> {
return handler.awaitList {
categoriesQueries.getVisibleCategoriesByMangaId(mangaId, categoryMapper)
categoriesQueries.getVisibleCategoriesByMangaId(mangaId, ::mapCategory)
}
}
override fun getCategoriesByMangaIdAsFlow(mangaId: Long): Flow<List<Category>> {
return handler.subscribeToList {
categoriesQueries.getCategoriesByMangaId(mangaId, categoryMapper)
categoriesQueries.getCategoriesByMangaId(mangaId, ::mapCategory)
}
}
override fun getVisibleCategoriesByMangaIdAsFlow(mangaId: Long): Flow<List<Category>> {
return handler.subscribeToList {
categoriesQueries.getVisibleCategoriesByMangaId(mangaId, categoryMapper)
categoriesQueries.getVisibleCategoriesByMangaId(mangaId, ::mapCategory)
}
}
@ -103,4 +102,20 @@ class MangaCategoryRepositoryImpl(
)
}
}
private fun mapCategory(
id: Long,
name: String,
order: Long,
flags: Long,
hidden: Long,
): Category {
return Category(
id = id,
name = name,
order = order,
flags = flags,
hidden = hidden == 1L,
)
}
}

View file

@ -4,120 +4,116 @@ import eu.kanade.tachiyomi.model.UpdateStrategy
import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.domain.library.anime.LibraryAnime
val animeMapper: (
Long,
Long,
String,
String?,
String?,
String?,
List<String>?,
String,
Long,
String?,
Boolean,
Long?,
Long?,
Boolean,
Long,
Long,
Long,
Long,
UpdateStrategy,
Long,
Long,
Long?,
) -> Anime =
{ id, source, url, artist, author, description, genre, title, status, thumbnailUrl, favorite, lastUpdate, nextUpdate, initialized, viewerFlags, episodeFlags, coverLastModified, dateAdded, updateStrategy, calculateInterval, lastModifiedAt, favoriteModifiedAt ->
Anime(
id = id,
source = source,
favorite = favorite,
lastUpdate = lastUpdate ?: 0,
nextUpdate = nextUpdate ?: 0,
fetchInterval = calculateInterval.toInt(),
dateAdded = dateAdded,
viewerFlags = viewerFlags,
episodeFlags = episodeFlags,
coverLastModified = coverLastModified,
url = url,
title = title,
artist = artist,
author = author,
description = description,
genre = genre,
status = status,
thumbnailUrl = thumbnailUrl,
updateStrategy = updateStrategy,
initialized = initialized,
lastModifiedAt = lastModifiedAt,
favoriteModifiedAt = favoriteModifiedAt,
)
}
object AnimeMapper {
fun mapAnime(
id: Long,
source: Long,
url: String,
artist: String?,
author: String?,
description: String?,
genre: List<String>?,
title: String,
status: Long,
thumbnailUrl: String?,
favorite: Boolean,
lastUpdate: Long?,
nextUpdate: Long?,
initialized: Boolean,
viewerFlags: Long,
chapterFlags: Long,
coverLastModified: Long,
dateAdded: Long,
updateStrategy: UpdateStrategy,
calculateInterval: Long,
lastModifiedAt: Long,
favoriteModifiedAt: Long?,
): Anime = Anime(
id = id,
source = source,
favorite = favorite,
lastUpdate = lastUpdate ?: 0,
nextUpdate = nextUpdate ?: 0,
fetchInterval = calculateInterval.toInt(),
dateAdded = dateAdded,
viewerFlags = viewerFlags,
episodeFlags = chapterFlags,
coverLastModified = coverLastModified,
url = url,
title = title,
artist = artist,
author = author,
description = description,
genre = genre,
status = status,
thumbnailUrl = thumbnailUrl,
updateStrategy = updateStrategy,
initialized = initialized,
lastModifiedAt = lastModifiedAt,
favoriteModifiedAt = favoriteModifiedAt,
)
val libraryAnime: (
Long,
Long,
String,
String?,
String?,
String?,
List<String>?,
String,
Long,
String?,
Boolean,
Long?,
Long?,
Boolean,
Long,
Long,
Long,
Long,
UpdateStrategy,
Long,
Long,
Long?,
Long,
Double,
Long,
Long,
Long,
Double,
Long,
) -> LibraryAnime =
{ id, source, url, artist, author, description, genre, title, status, thumbnailUrl, favorite, lastUpdate, nextUpdate, initialized, viewerFlags, episodeFlags, coverLastModified, dateAdded, updateStrategy, calculateInterval, lastModifiedAt, favoriteModifiedAt, totalCount, seenCount, latestUpload, episodeFetchedAt, lastSeen, bookmarkCount, category ->
LibraryAnime(
anime = animeMapper(
id,
source,
url,
artist,
author,
description,
genre,
title,
status,
thumbnailUrl,
favorite,
lastUpdate,
nextUpdate,
initialized,
viewerFlags,
episodeFlags,
coverLastModified,
dateAdded,
updateStrategy,
calculateInterval,
lastModifiedAt,
favoriteModifiedAt,
),
category = category,
totalEpisodes = totalCount,
seenCount = seenCount.toLong(),
bookmarkCount = bookmarkCount.toLong(),
latestUpload = latestUpload,
episodeFetchedAt = episodeFetchedAt,
lastSeen = lastSeen,
)
}
fun mapLibraryAnime(
id: Long,
source: Long,
url: String,
artist: String?,
author: String?,
description: String?,
genre: List<String>?,
title: String,
status: Long,
thumbnailUrl: String?,
favorite: Boolean,
lastUpdate: Long?,
nextUpdate: Long?,
initialized: Boolean,
viewerFlags: Long,
chapterFlags: Long,
coverLastModified: Long,
dateAdded: Long,
updateStrategy: UpdateStrategy,
calculateInterval: Long,
lastModifiedAt: Long,
favoriteModifiedAt: Long?,
totalCount: Long,
seenCount: Double,
latestUpload: Long,
episodeFetchedAt: Long,
lastSeen: Long,
bookmarkCount: Double,
category: Long,
): LibraryAnime = LibraryAnime(
anime = mapAnime(
id,
source,
url,
artist,
author,
description,
genre,
title,
status,
thumbnailUrl,
favorite,
lastUpdate,
nextUpdate,
initialized,
viewerFlags,
chapterFlags,
coverLastModified,
dateAdded,
updateStrategy,
calculateInterval,
lastModifiedAt,
favoriteModifiedAt,
),
category = category,
totalEpisodes = totalCount,
seenCount = seenCount.toLong(),
bookmarkCount = bookmarkCount.toLong(),
latestUpload = latestUpload,
episodeFetchedAt = episodeFetchedAt,
lastSeen = lastSeen,
)
}

View file

@ -17,11 +17,11 @@ class AnimeRepositoryImpl(
) : AnimeRepository {
override suspend fun getAnimeById(id: Long): Anime {
return handler.awaitOne { animesQueries.getAnimeById(id, animeMapper) }
return handler.awaitOne { animesQueries.getAnimeById(id, AnimeMapper::mapAnime) }
}
override suspend fun getAnimeByIdAsFlow(id: Long): Flow<Anime> {
return handler.subscribeToOne { animesQueries.getAnimeById(id, animeMapper) }
return handler.subscribeToOne { animesQueries.getAnimeById(id, AnimeMapper::mapAnime) }
}
override suspend fun getAnimeByUrlAndSourceId(url: String, sourceId: Long): Anime? {
@ -29,7 +29,7 @@ class AnimeRepositoryImpl(
animesQueries.getAnimeByUrlAndSource(
url,
sourceId,
animeMapper,
AnimeMapper::mapAnime,
)
}
}
@ -39,30 +39,30 @@ class AnimeRepositoryImpl(
animesQueries.getAnimeByUrlAndSource(
url,
sourceId,
animeMapper,
AnimeMapper::mapAnime,
)
}
}
override suspend fun getAnimeFavorites(): List<Anime> {
return handler.awaitList { animesQueries.getFavorites(animeMapper) }
return handler.awaitList { animesQueries.getFavorites(AnimeMapper::mapAnime) }
}
override suspend fun getLibraryAnime(): List<LibraryAnime> {
return handler.awaitList { animelibViewQueries.animelib(libraryAnime) }
return handler.awaitList { animelibViewQueries.animelib(AnimeMapper::mapLibraryAnime) }
}
override fun getLibraryAnimeAsFlow(): Flow<List<LibraryAnime>> {
return handler.subscribeToList { animelibViewQueries.animelib(libraryAnime) }
return handler.subscribeToList { animelibViewQueries.animelib(AnimeMapper::mapLibraryAnime) }
}
override fun getAnimeFavoritesBySourceId(sourceId: Long): Flow<List<Anime>> {
return handler.subscribeToList { animesQueries.getFavoriteBySourceId(sourceId, animeMapper) }
return handler.subscribeToList { animesQueries.getFavoriteBySourceId(sourceId, AnimeMapper::mapAnime) }
}
override suspend fun getDuplicateLibraryAnime(id: Long, title: String): List<Anime> {
return handler.awaitList {
animesQueries.getDuplicateLibraryAnime(title, id, animeMapper)
animesQueries.getDuplicateLibraryAnime(title, id, AnimeMapper::mapAnime)
}
}

View file

@ -4,120 +4,116 @@ import eu.kanade.tachiyomi.model.UpdateStrategy
import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.library.manga.LibraryManga
val mangaMapper: (
Long,
Long,
String,
String?,
String?,
String?,
List<String>?,
String,
Long,
String?,
Boolean,
Long?,
Long?,
Boolean,
Long,
Long,
Long,
Long,
UpdateStrategy,
Long,
Long,
Long?,
) -> Manga =
{ id, source, url, artist, author, description, genre, title, status, thumbnailUrl, favorite, lastUpdate, nextUpdate, initialized, viewerFlags, chapterFlags, coverLastModified, dateAdded, updateStrategy, calculateInterval, lastModifiedAt, favoriteModifiedAt ->
Manga(
id = id,
source = source,
favorite = favorite,
lastUpdate = lastUpdate ?: 0,
nextUpdate = nextUpdate ?: 0,
fetchInterval = calculateInterval.toInt(),
dateAdded = dateAdded,
viewerFlags = viewerFlags,
chapterFlags = chapterFlags,
coverLastModified = coverLastModified,
url = url,
title = title,
artist = artist,
author = author,
description = description,
genre = genre,
status = status,
thumbnailUrl = thumbnailUrl,
updateStrategy = updateStrategy,
initialized = initialized,
lastModifiedAt = lastModifiedAt,
favoriteModifiedAt = favoriteModifiedAt,
)
}
object MangaMapper {
fun mapManga(
id: Long,
source: Long,
url: String,
artist: String?,
author: String?,
description: String?,
genre: List<String>?,
title: String,
status: Long,
thumbnailUrl: String?,
favorite: Boolean,
lastUpdate: Long?,
nextUpdate: Long?,
initialized: Boolean,
viewerFlags: Long,
chapterFlags: Long,
coverLastModified: Long,
dateAdded: Long,
updateStrategy: UpdateStrategy,
calculateInterval: Long,
lastModifiedAt: Long,
favoriteModifiedAt: Long?,
): Manga = Manga(
id = id,
source = source,
favorite = favorite,
lastUpdate = lastUpdate ?: 0,
nextUpdate = nextUpdate ?: 0,
fetchInterval = calculateInterval.toInt(),
dateAdded = dateAdded,
viewerFlags = viewerFlags,
chapterFlags = chapterFlags,
coverLastModified = coverLastModified,
url = url,
title = title,
artist = artist,
author = author,
description = description,
genre = genre,
status = status,
thumbnailUrl = thumbnailUrl,
updateStrategy = updateStrategy,
initialized = initialized,
lastModifiedAt = lastModifiedAt,
favoriteModifiedAt = favoriteModifiedAt,
)
val libraryManga: (
Long,
Long,
String,
String?,
String?,
String?,
List<String>?,
String,
Long,
String?,
Boolean,
Long?,
Long?,
Boolean,
Long,
Long,
Long,
Long,
UpdateStrategy,
Long,
Long,
Long?,
Long,
Double,
Long,
Long,
Long,
Double,
Long,
) -> LibraryManga =
{ id, source, url, artist, author, description, genre, title, status, thumbnailUrl, favorite, lastUpdate, nextUpdate, initialized, viewerFlags, chapterFlags, coverLastModified, dateAdded, updateStrategy, calculateInterval, lastModifiedAt, favoriteModifiedAt, totalCount, readCount, latestUpload, chapterFetchedAt, lastRead, bookmarkCount, category ->
LibraryManga(
manga = mangaMapper(
id,
source,
url,
artist,
author,
description,
genre,
title,
status,
thumbnailUrl,
favorite,
lastUpdate,
nextUpdate,
initialized,
viewerFlags,
chapterFlags,
coverLastModified,
dateAdded,
updateStrategy,
calculateInterval,
lastModifiedAt,
favoriteModifiedAt,
),
category = category,
totalChapters = totalCount,
readCount = readCount.toLong(),
bookmarkCount = bookmarkCount.toLong(),
latestUpload = latestUpload,
chapterFetchedAt = chapterFetchedAt,
lastRead = lastRead,
)
}
fun mapLibraryManga(
id: Long,
source: Long,
url: String,
artist: String?,
author: String?,
description: String?,
genre: List<String>?,
title: String,
status: Long,
thumbnailUrl: String?,
favorite: Boolean,
lastUpdate: Long?,
nextUpdate: Long?,
initialized: Boolean,
viewerFlags: Long,
chapterFlags: Long,
coverLastModified: Long,
dateAdded: Long,
updateStrategy: UpdateStrategy,
calculateInterval: Long,
lastModifiedAt: Long,
favoriteModifiedAt: Long?,
totalCount: Long,
readCount: Double,
latestUpload: Long,
chapterFetchedAt: Long,
lastRead: Long,
bookmarkCount: Double,
category: Long,
): LibraryManga = LibraryManga(
manga = mapManga(
id,
source,
url,
artist,
author,
description,
genre,
title,
status,
thumbnailUrl,
favorite,
lastUpdate,
nextUpdate,
initialized,
viewerFlags,
chapterFlags,
coverLastModified,
dateAdded,
updateStrategy,
calculateInterval,
lastModifiedAt,
favoriteModifiedAt,
),
category = category,
totalChapters = totalCount,
readCount = readCount.toLong(),
bookmarkCount = bookmarkCount.toLong(),
latestUpload = latestUpload,
chapterFetchedAt = chapterFetchedAt,
lastRead = lastRead,
)
}

View file

@ -17,11 +17,11 @@ class MangaRepositoryImpl(
) : MangaRepository {
override suspend fun getMangaById(id: Long): Manga {
return handler.awaitOne { mangasQueries.getMangaById(id, mangaMapper) }
return handler.awaitOne { mangasQueries.getMangaById(id, MangaMapper::mapManga) }
}
override suspend fun getMangaByIdAsFlow(id: Long): Flow<Manga> {
return handler.subscribeToOne { mangasQueries.getMangaById(id, mangaMapper) }
return handler.subscribeToOne { mangasQueries.getMangaById(id, MangaMapper::mapManga) }
}
override suspend fun getMangaByUrlAndSourceId(url: String, sourceId: Long): Manga? {
@ -29,7 +29,7 @@ class MangaRepositoryImpl(
mangasQueries.getMangaByUrlAndSource(
url,
sourceId,
mangaMapper,
MangaMapper::mapManga,
)
}
}
@ -39,30 +39,30 @@ class MangaRepositoryImpl(
mangasQueries.getMangaByUrlAndSource(
url,
sourceId,
mangaMapper,
MangaMapper::mapManga,
)
}
}
override suspend fun getMangaFavorites(): List<Manga> {
return handler.awaitList { mangasQueries.getFavorites(mangaMapper) }
return handler.awaitList { mangasQueries.getFavorites(MangaMapper::mapManga) }
}
override suspend fun getLibraryManga(): List<LibraryManga> {
return handler.awaitList { libraryViewQueries.library(libraryManga) }
return handler.awaitList { libraryViewQueries.library(MangaMapper::mapLibraryManga) }
}
override fun getLibraryMangaAsFlow(): Flow<List<LibraryManga>> {
return handler.subscribeToList { libraryViewQueries.library(libraryManga) }
return handler.subscribeToList { libraryViewQueries.library(MangaMapper::mapLibraryManga) }
}
override fun getMangaFavoritesBySourceId(sourceId: Long): Flow<List<Manga>> {
return handler.subscribeToList { mangasQueries.getFavoriteBySourceId(sourceId, mangaMapper) }
return handler.subscribeToList { mangasQueries.getFavoriteBySourceId(sourceId, MangaMapper::mapManga) }
}
override suspend fun getDuplicateLibraryManga(id: Long, title: String): List<Manga> {
return handler.awaitList {
mangasQueries.getDuplicateLibraryManga(title, id, mangaMapper)
mangasQueries.getDuplicateLibraryManga(title, id, MangaMapper::mapManga)
}
}

View file

@ -5,28 +5,29 @@ import tachiyomi.domain.history.anime.model.AnimeHistory
import tachiyomi.domain.history.anime.model.AnimeHistoryWithRelations
import java.util.Date
val animeHistoryMapper: (Long, Long, Date?) -> AnimeHistory = { id, episodeId, seenAt ->
AnimeHistory(
object AnimeHistoryMapper {
fun mapAnimeHistory(
id: Long,
episodeId: Long,
seenAt: Date?,
): AnimeHistory = AnimeHistory(
id = id,
episodeId = episodeId,
seenAt = seenAt,
)
}
val animeHistoryWithRelationsMapper: (
Long,
Long,
Long,
String,
String?,
Long,
Boolean,
Long,
Double,
Date?,
) -> AnimeHistoryWithRelations = {
historyId, animeId, episodeId, title, thumbnailUrl, sourceId, isFavorite, coverLastModified, episodeNumber, seenAt ->
AnimeHistoryWithRelations(
fun mapAnimeHistoryWithRelations(
historyId: Long,
animeId: Long,
episodeId: Long,
title: String,
thumbnailUrl: String?,
sourceId: Long,
isFavorite: Boolean,
coverLastModified: Long,
episodeNumber: Double,
seenAt: Date?,
): AnimeHistoryWithRelations = AnimeHistoryWithRelations(
id = historyId,
episodeId = episodeId,
animeId = animeId,

View file

@ -15,13 +15,13 @@ class AnimeHistoryRepositoryImpl(
override fun getAnimeHistory(query: String): Flow<List<AnimeHistoryWithRelations>> {
return handler.subscribeToList {
animehistoryViewQueries.animehistory(query, animeHistoryWithRelationsMapper)
animehistoryViewQueries.animehistory(query, AnimeHistoryMapper::mapAnimeHistoryWithRelations)
}
}
override suspend fun getLastAnimeHistory(): AnimeHistoryWithRelations? {
return handler.awaitOneOrNull {
animehistoryViewQueries.getLatestAnimeHistory(animeHistoryWithRelationsMapper)
animehistoryViewQueries.getLatestAnimeHistory(AnimeHistoryMapper::mapAnimeHistoryWithRelations)
}
}
@ -29,7 +29,7 @@ class AnimeHistoryRepositoryImpl(
return handler.awaitList {
animehistoryQueries.getHistoryByAnimeId(
animeId,
animeHistoryMapper,
AnimeHistoryMapper::mapAnimeHistory,
)
}
}

View file

@ -5,30 +5,32 @@ import tachiyomi.domain.history.manga.model.MangaHistory
import tachiyomi.domain.history.manga.model.MangaHistoryWithRelations
import java.util.Date
val mangaHistoryMapper: (Long, Long, Date?, Long) -> MangaHistory = { id, chapterId, readAt, readDuration ->
MangaHistory(
object MangaHistoryMapper {
fun mapMangaHistory(
id: Long,
chapterId: Long,
readAt: Date?,
readDuration: Long,
): MangaHistory = MangaHistory(
id = id,
chapterId = chapterId,
readAt = readAt,
readDuration = readDuration,
)
}
val mangaHistoryWithRelationsMapper: (
Long,
Long,
Long,
String,
String?,
Long,
Boolean,
Long,
Double,
Date?,
Long,
) -> MangaHistoryWithRelations = {
historyId, mangaId, chapterId, title, thumbnailUrl, sourceId, isFavorite, coverLastModified, chapterNumber, readAt, readDuration ->
MangaHistoryWithRelations(
fun mapMangaHistoryWithRelations(
historyId: Long,
mangaId: Long,
chapterId: Long,
title: String,
thumbnailUrl: String?,
sourceId: Long,
isFavorite: Boolean,
coverLastModified: Long,
chapterNumber: Double,
readAt: Date?,
readDuration: Long,
): MangaHistoryWithRelations = MangaHistoryWithRelations(
id = historyId,
chapterId = chapterId,
mangaId = mangaId,

View file

@ -15,13 +15,13 @@ class MangaHistoryRepositoryImpl(
override fun getMangaHistory(query: String): Flow<List<MangaHistoryWithRelations>> {
return handler.subscribeToList {
historyViewQueries.history(query, mangaHistoryWithRelationsMapper)
historyViewQueries.history(query, MangaHistoryMapper::mapMangaHistoryWithRelations)
}
}
override suspend fun getLastMangaHistory(): MangaHistoryWithRelations? {
return handler.awaitOneOrNull {
historyViewQueries.getLatestHistory(mangaHistoryWithRelationsMapper)
historyViewQueries.getLatestHistory(MangaHistoryMapper::mapMangaHistoryWithRelations)
}
}
@ -30,7 +30,7 @@ class MangaHistoryRepositoryImpl(
}
override suspend fun getHistoryByMangaId(mangaId: Long): List<MangaHistory> {
return handler.awaitList { historyQueries.getHistoryByMangaId(mangaId, mangaHistoryMapper) }
return handler.awaitList { historyQueries.getHistoryByMangaId(mangaId, MangaHistoryMapper::mapMangaHistory) }
}
override suspend fun resetMangaHistory(historyId: Long) {

View file

@ -1,36 +0,0 @@
package tachiyomi.data.items.chapter
import tachiyomi.domain.items.chapter.model.Chapter
val chapterMapper: (
Long,
Long,
String,
String,
String?,
Boolean,
Boolean,
Long,
Double,
Long,
Long,
Long,
Long,
) -> Chapter =
{ id, mangaId, url, name, scanlator, read, bookmark, lastPageRead, chapterNumber, sourceOrder, dateFetch, dateUpload, lastModifiedAt ->
Chapter(
id = id,
mangaId = mangaId,
read = read,
bookmark = bookmark,
lastPageRead = lastPageRead,
dateFetch = dateFetch,
sourceOrder = sourceOrder,
url = url,
name = name,
dateUpload = dateUpload,
chapterNumber = chapterNumber,
scanlator = scanlator,
lastModifiedAt = lastModifiedAt,
)
}

View file

@ -77,27 +77,27 @@ class ChapterRepositoryImpl(
}
override suspend fun getChapterByMangaId(mangaId: Long): List<Chapter> {
return handler.awaitList { chaptersQueries.getChaptersByMangaId(mangaId, chapterMapper) }
return handler.awaitList { chaptersQueries.getChaptersByMangaId(mangaId, ::mapChapter) }
}
override suspend fun getBookmarkedChaptersByMangaId(mangaId: Long): List<Chapter> {
return handler.awaitList {
chaptersQueries.getBookmarkedChaptersByMangaId(
mangaId,
chapterMapper,
::mapChapter,
)
}
}
override suspend fun getChapterById(id: Long): Chapter? {
return handler.awaitOneOrNull { chaptersQueries.getChapterById(id, chapterMapper) }
return handler.awaitOneOrNull { chaptersQueries.getChapterById(id, ::mapChapter) }
}
override suspend fun getChapterByMangaIdAsFlow(mangaId: Long): Flow<List<Chapter>> {
return handler.subscribeToList {
chaptersQueries.getChaptersByMangaId(
mangaId,
chapterMapper,
::mapChapter,
)
}
}
@ -107,8 +107,38 @@ class ChapterRepositoryImpl(
chaptersQueries.getChapterByUrlAndMangaId(
url,
mangaId,
chapterMapper,
::mapChapter,
)
}
}
private fun mapChapter(
id: Long,
mangaId: Long,
url: String,
name: String,
scanlator: String?,
read: Boolean,
bookmark: Boolean,
lastPageRead: Long,
chapterNumber: Double,
sourceOrder: Long,
dateFetch: Long,
dateUpload: Long,
lastModifiedAt: Long,
): Chapter = Chapter(
id = id,
mangaId = mangaId,
read = read,
bookmark = bookmark,
lastPageRead = lastPageRead,
dateFetch = dateFetch,
sourceOrder = sourceOrder,
url = url,
name = name,
dateUpload = dateUpload,
chapterNumber = chapterNumber,
scanlator = scanlator,
lastModifiedAt = lastModifiedAt,
)
}

View file

@ -1,38 +0,0 @@
package tachiyomi.data.items.episode
import tachiyomi.domain.items.episode.model.Episode
val episodeMapper: (
Long,
Long,
String,
String,
String?,
Boolean,
Boolean,
Long,
Long,
Double,
Long,
Long,
Long,
Long,
) -> Episode =
{ id, animeId, url, name, scanlator, seen, bookmark, lastSecondSeen, totalSeconds, episodeNumber, sourceOrder, dateFetch, dateUpload, lastModifiedAt ->
Episode(
id = id,
animeId = animeId,
seen = seen,
bookmark = bookmark,
lastSecondSeen = lastSecondSeen,
totalSeconds = totalSeconds,
dateFetch = dateFetch,
sourceOrder = sourceOrder,
url = url,
name = name,
dateUpload = dateUpload,
episodeNumber = episodeNumber,
scanlator = scanlator,
lastModifiedAt = lastModifiedAt,
)
}

View file

@ -79,27 +79,27 @@ class EpisodeRepositoryImpl(
}
override suspend fun getEpisodeByAnimeId(animeId: Long): List<Episode> {
return handler.awaitList { episodesQueries.getEpisodesByAnimeId(animeId, episodeMapper) }
return handler.awaitList { episodesQueries.getEpisodesByAnimeId(animeId, ::mapEpisode) }
}
override suspend fun getBookmarkedEpisodesByAnimeId(animeId: Long): List<Episode> {
return handler.awaitList {
episodesQueries.getBookmarkedEpisodesByAnimeId(
animeId,
episodeMapper,
::mapEpisode,
)
}
}
override suspend fun getEpisodeById(id: Long): Episode? {
return handler.awaitOneOrNull { episodesQueries.getEpisodeById(id, episodeMapper) }
return handler.awaitOneOrNull { episodesQueries.getEpisodeById(id, ::mapEpisode) }
}
override suspend fun getEpisodeByAnimeIdAsFlow(animeId: Long): Flow<List<Episode>> {
return handler.subscribeToList {
episodesQueries.getEpisodesByAnimeId(
animeId,
episodeMapper,
::mapEpisode,
)
}
}
@ -109,8 +109,40 @@ class EpisodeRepositoryImpl(
episodesQueries.getEpisodeByUrlAndAnimeId(
url,
animeId,
episodeMapper,
::mapEpisode,
)
}
}
private fun mapEpisode(
id: Long,
animeId: Long,
url: String,
name: String,
scanlator: String?,
seen: Boolean,
bookmark: Boolean,
lastSecondSeen: Long,
totalSeconds: Long,
episodeNumber: Double,
sourceOrder: Long,
dateFetch: Long,
dateUpload: Long,
lastModifiedAt: Long,
): Episode = Episode(
id = id,
animeId = animeId ,
seen = seen ,
bookmark = bookmark,
lastSecondSeen = lastSecondSeen ,
totalSeconds = totalSeconds,
dateFetch = dateFetch,
sourceOrder = sourceOrder,
url = url,
name = name,
dateUpload = dateUpload,
episodeNumber = episodeNumber,
scanlator = scanlator,
lastModifiedAt = lastModifiedAt,
)
}

View file

@ -1,18 +0,0 @@
package tachiyomi.data.source.anime
import tachiyomi.domain.source.anime.model.AnimeSource
import tachiyomi.domain.source.anime.model.StubAnimeSource
val animeSourceMapper: (eu.kanade.tachiyomi.animesource.AnimeSource) -> AnimeSource = { source ->
AnimeSource(
id = source.id,
lang = source.lang,
name = source.name,
supportsLatest = false,
isStub = false,
)
}
val animeSourceDataMapper: (Long, String, String) -> StubAnimeSource = { id, lang, name ->
StubAnimeSource(id, lang, name)
}

View file

@ -1,48 +1,50 @@
package tachiyomi.data.source.anime
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
import eu.kanade.tachiyomi.animesource.AnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import tachiyomi.data.handlers.anime.AnimeDatabaseHandler
import tachiyomi.domain.source.anime.model.AnimeSource
import tachiyomi.domain.source.anime.model.AnimeSourceWithCount
import tachiyomi.domain.source.anime.model.StubAnimeSource
import tachiyomi.domain.source.anime.repository.AnimeSourcePagingSourceType
import tachiyomi.domain.source.anime.repository.AnimeSourceRepository
import tachiyomi.domain.source.anime.service.AnimeSourceManager
import tachiyomi.domain.source.anime.model.AnimeSource as DomainSource
class AnimeSourceRepositoryImpl(
private val sourceManager: AnimeSourceManager,
private val handler: AnimeDatabaseHandler,
) : AnimeSourceRepository {
override fun getAnimeSources(): Flow<List<AnimeSource>> {
override fun getAnimeSources(): Flow<List<DomainSource>> {
return sourceManager.catalogueSources.map { sources ->
sources.map {
animeSourceMapper(it).copy(
mapSourceToDomainSource(it).copy(
supportsLatest = it.supportsLatest,
)
}
}
}
override fun getOnlineAnimeSources(): Flow<List<AnimeSource>> {
override fun getOnlineAnimeSources(): Flow<List<DomainSource>> {
return sourceManager.catalogueSources.map { sources ->
sources
.filterIsInstance<AnimeHttpSource>()
.map(animeSourceMapper)
.map(::mapSourceToDomainSource)
}
}
override fun getAnimeSourcesWithFavoriteCount(): Flow<List<Pair<AnimeSource, Long>>> {
val sourceIdWithFavoriteCount = handler.subscribeToList { animesQueries.getAnimeSourceIdWithFavoriteCount() }
override fun getAnimeSourcesWithFavoriteCount(): Flow<List<Pair<DomainSource, Long>>> {
val sourceIdWithFavoriteCount =
handler.subscribeToList { animesQueries.getAnimeSourceIdWithFavoriteCount() }
return sourceIdWithFavoriteCount.map { sourceIdsWithCount ->
sourceIdsWithCount
.map { (sourceId, count) ->
val source = sourceManager.getOrStub(sourceId)
val domainSource = animeSourceMapper(source).copy(
val domainSource = mapSourceToDomainSource(source).copy(
isStub = source is StubAnimeSource,
)
domainSource to count
@ -51,11 +53,12 @@ class AnimeSourceRepositoryImpl(
}
override fun getSourcesWithNonLibraryAnime(): Flow<List<AnimeSourceWithCount>> {
val sourceIdWithNonLibraryAnime = handler.subscribeToList { animesQueries.getSourceIdsWithNonLibraryAnime() }
val sourceIdWithNonLibraryAnime =
handler.subscribeToList { animesQueries.getSourceIdsWithNonLibraryAnime() }
return sourceIdWithNonLibraryAnime.map { sourceId ->
sourceId.map { (sourceId, count) ->
val source = sourceManager.getOrStub(sourceId)
val domainSource = animeSourceMapper(source).copy(
val domainSource = mapSourceToDomainSource(source).copy(
isStub = source is StubAnimeSource,
)
AnimeSourceWithCount(domainSource, count)
@ -81,4 +84,12 @@ class AnimeSourceRepositoryImpl(
val source = sourceManager.get(sourceId) as AnimeCatalogueSource
return AnimeSourceLatestPagingSource(source)
}
private fun mapSourceToDomainSource(source: AnimeSource): DomainSource = DomainSource(
id = source.id,
lang = source.lang,
name = source.name,
supportsLatest = false,
isStub = false,
)
}

View file

@ -10,14 +10,14 @@ class AnimeStubSourceRepositoryImpl(
) : AnimeStubSourceRepository {
override fun subscribeAllAnime(): Flow<List<StubAnimeSource>> {
return handler.subscribeToList { animesourcesQueries.findAll(animeSourceDataMapper) }
return handler.subscribeToList { animesourcesQueries.findAll(::mapStubSource) }
}
override suspend fun getStubAnimeSource(id: Long): StubAnimeSource? {
return handler.awaitOneOrNull {
animesourcesQueries.findOne(
id,
animeSourceDataMapper,
::mapStubSource,
)
}
}
@ -25,4 +25,10 @@ class AnimeStubSourceRepositoryImpl(
override suspend fun upsertStubAnimeSource(id: Long, lang: String, name: String) {
handler.await { animesourcesQueries.upsert(id, lang, name) }
}
private fun mapStubSource(
id: Long,
lang: String,
name: String,
): StubAnimeSource = StubAnimeSource(id = id, lang = lang, name = name)
}

View file

@ -1,18 +0,0 @@
package tachiyomi.data.source.manga
import tachiyomi.domain.source.manga.model.Source
import tachiyomi.domain.source.manga.model.StubMangaSource
val mangaSourceMapper: (eu.kanade.tachiyomi.source.MangaSource) -> Source = { source ->
Source(
id = source.id,
lang = source.lang,
name = source.name,
supportsLatest = false,
isStub = false,
)
}
val mangaSourceDataMapper: (Long, String, String) -> StubMangaSource = { id, lang, name ->
StubMangaSource(id, lang, name)
}

View file

@ -1,48 +1,50 @@
package tachiyomi.data.source.manga
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.MangaSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import tachiyomi.data.handlers.manga.MangaDatabaseHandler
import tachiyomi.domain.source.manga.model.MangaSourceWithCount
import tachiyomi.domain.source.manga.model.Source
import tachiyomi.domain.source.manga.model.StubMangaSource
import tachiyomi.domain.source.manga.repository.MangaSourceRepository
import tachiyomi.domain.source.manga.repository.SourcePagingSourceType
import tachiyomi.domain.source.manga.service.MangaSourceManager
import tachiyomi.domain.source.manga.model.Source as DomainSource
class MangaSourceRepositoryImpl(
private val sourceManager: MangaSourceManager,
private val handler: MangaDatabaseHandler,
) : MangaSourceRepository {
override fun getMangaSources(): Flow<List<Source>> {
override fun getMangaSources(): Flow<List<DomainSource>> {
return sourceManager.catalogueSources.map { sources ->
sources.map {
mangaSourceMapper(it).copy(
mapSourceToDomainSource(it).copy(
supportsLatest = it.supportsLatest,
)
}
}
}
override fun getOnlineMangaSources(): Flow<List<Source>> {
override fun getOnlineMangaSources(): Flow<List<DomainSource>> {
return sourceManager.catalogueSources.map { sources ->
sources
.filterIsInstance<HttpSource>()
.map(mangaSourceMapper)
.map(::mapSourceToDomainSource)
}
}
override fun getMangaSourcesWithFavoriteCount(): Flow<List<Pair<Source, Long>>> {
val sourceIdWithFavoriteCount = handler.subscribeToList { mangasQueries.getSourceIdWithFavoriteCount() }
override fun getMangaSourcesWithFavoriteCount(): Flow<List<Pair<DomainSource, Long>>> {
val sourceIdWithFavoriteCount =
handler.subscribeToList { mangasQueries.getSourceIdWithFavoriteCount() }
return sourceIdWithFavoriteCount.map { sourceIdsWithCount ->
sourceIdsWithCount
.map { (sourceId, count) ->
val source = sourceManager.getOrStub(sourceId)
val domainSource = mangaSourceMapper(source).copy(
val domainSource = mapSourceToDomainSource(source).copy(
isStub = source is StubMangaSource,
)
domainSource to count
@ -51,11 +53,12 @@ class MangaSourceRepositoryImpl(
}
override fun getMangaSourcesWithNonLibraryManga(): Flow<List<MangaSourceWithCount>> {
val sourceIdWithNonLibraryManga = handler.subscribeToList { mangasQueries.getSourceIdsWithNonLibraryManga() }
val sourceIdWithNonLibraryManga =
handler.subscribeToList { mangasQueries.getSourceIdsWithNonLibraryManga() }
return sourceIdWithNonLibraryManga.map { sourceId ->
sourceId.map { (sourceId, count) ->
val source = sourceManager.getOrStub(sourceId)
val domainSource = mangaSourceMapper(source).copy(
val domainSource = mapSourceToDomainSource(source).copy(
isStub = source is StubMangaSource,
)
MangaSourceWithCount(domainSource, count)
@ -81,4 +84,12 @@ class MangaSourceRepositoryImpl(
val source = sourceManager.get(sourceId) as CatalogueSource
return SourceLatestPagingSource(source)
}
private fun mapSourceToDomainSource(source: MangaSource): DomainSource = DomainSource(
id = source.id,
lang = source.lang,
name = source.name,
supportsLatest = false,
isStub = false,
)
}

View file

@ -10,14 +10,14 @@ class MangaStubSourceRepositoryImpl(
) : MangaStubSourceRepository {
override fun subscribeAllManga(): Flow<List<StubMangaSource>> {
return handler.subscribeToList { sourcesQueries.findAll(mangaSourceDataMapper) }
return handler.subscribeToList { sourcesQueries.findAll(::mapStubSource) }
}
override suspend fun getStubMangaSource(id: Long): StubMangaSource? {
return handler.awaitOneOrNull {
sourcesQueries.findOne(
id,
mangaSourceDataMapper,
::mapStubSource,
)
}
}
@ -25,4 +25,10 @@ class MangaStubSourceRepositoryImpl(
override suspend fun upsertStubMangaSource(id: Long, lang: String, name: String) {
handler.await { sourcesQueries.upsert(id, lang, name) }
}
private fun mapStubSource(
id: Long,
lang: String,
name: String,
): StubMangaSource = StubMangaSource(id = id, lang = lang, name = name)
}

View file

@ -1,36 +0,0 @@
package tachiyomi.data.track.anime
import tachiyomi.domain.track.anime.model.AnimeTrack
val animeTrackMapper: (
Long,
Long,
Long,
Long,
Long?,
String,
Double,
Long,
Long,
Double,
String,
Long,
Long,
) -> AnimeTrack =
{ id, animeId, syncId, remoteId, libraryId, title, lastEpisodeSeen, totalEpisodes, status, score, remoteUrl, startDate, finishDate ->
AnimeTrack(
id = id,
animeId = animeId,
syncId = syncId,
remoteId = remoteId,
libraryId = libraryId,
title = title,
lastEpisodeSeen = lastEpisodeSeen,
totalEpisodes = totalEpisodes,
status = status,
score = score,
remoteUrl = remoteUrl,
startDate = startDate,
finishDate = finishDate,
)
}

View file

@ -10,24 +10,24 @@ class AnimeTrackRepositoryImpl(
) : AnimeTrackRepository {
override suspend fun getTrackByAnimeId(id: Long): AnimeTrack? {
return handler.awaitOneOrNull { anime_syncQueries.getTrackByAnimeId(id, animeTrackMapper) }
return handler.awaitOneOrNull { anime_syncQueries.getTrackByAnimeId(id, ::mapTrack) }
}
override suspend fun getTracksByAnimeId(animeId: Long): List<AnimeTrack> {
return handler.awaitList {
anime_syncQueries.getTracksByAnimeId(animeId, animeTrackMapper)
anime_syncQueries.getTracksByAnimeId(animeId, ::mapTrack)
}
}
override fun getAnimeTracksAsFlow(): Flow<List<AnimeTrack>> {
return handler.subscribeToList {
anime_syncQueries.getAnimeTracks(animeTrackMapper)
anime_syncQueries.getAnimeTracks(::mapTrack)
}
}
override fun getTracksByAnimeIdAsFlow(animeId: Long): Flow<List<AnimeTrack>> {
return handler.subscribeToList {
anime_syncQueries.getTracksByAnimeId(animeId, animeTrackMapper)
anime_syncQueries.getTracksByAnimeId(animeId, ::mapTrack)
}
}
@ -68,4 +68,34 @@ class AnimeTrackRepositoryImpl(
}
}
}
private fun mapTrack(
id: Long,
animeId: Long,
syncId: Long,
remoteId: Long,
libraryId: Long?,
title: String,
lastEpisodeSeen: Double,
totalEpisodes: Long,
status: Long,
score: Double,
remoteUrl: String,
startDate: Long,
finishDate: Long,
): AnimeTrack = AnimeTrack(
id = id,
animeId = animeId,
syncId = syncId,
remoteId = remoteId,
libraryId = libraryId,
title = title,
lastEpisodeSeen = lastEpisodeSeen,
totalEpisodes = totalEpisodes,
status = status,
score = score,
remoteUrl = remoteUrl,
startDate = startDate,
finishDate = finishDate,
)
}

View file

@ -1,36 +0,0 @@
package tachiyomi.data.track.manga
import tachiyomi.domain.track.manga.model.MangaTrack
val mangaTrackMapper: (
Long,
Long,
Long,
Long,
Long?,
String,
Double,
Long,
Long,
Double,
String,
Long,
Long,
) -> MangaTrack =
{ id, mangaId, syncId, remoteId, libraryId, title, lastChapterRead, totalChapters, status, score, remoteUrl, startDate, finishDate ->
MangaTrack(
id = id,
mangaId = mangaId,
syncId = syncId,
remoteId = remoteId,
libraryId = libraryId,
title = title,
lastChapterRead = lastChapterRead,
totalChapters = totalChapters,
status = status,
score = score,
remoteUrl = remoteUrl,
startDate = startDate,
finishDate = finishDate,
)
}

View file

@ -10,24 +10,24 @@ class MangaTrackRepositoryImpl(
) : MangaTrackRepository {
override suspend fun getTrackByMangaId(id: Long): MangaTrack? {
return handler.awaitOneOrNull { manga_syncQueries.getTrackById(id, mangaTrackMapper) }
return handler.awaitOneOrNull { manga_syncQueries.getTrackById(id, ::mapTrack) }
}
override suspend fun getTracksByMangaId(mangaId: Long): List<MangaTrack> {
return handler.awaitList {
manga_syncQueries.getTracksByMangaId(mangaId, mangaTrackMapper)
manga_syncQueries.getTracksByMangaId(mangaId, ::mapTrack)
}
}
override fun getMangaTracksAsFlow(): Flow<List<MangaTrack>> {
return handler.subscribeToList {
manga_syncQueries.getTracks(mangaTrackMapper)
manga_syncQueries.getTracks(::mapTrack)
}
}
override fun getTracksByMangaIdAsFlow(mangaId: Long): Flow<List<MangaTrack>> {
return handler.subscribeToList {
manga_syncQueries.getTracksByMangaId(mangaId, mangaTrackMapper)
manga_syncQueries.getTracksByMangaId(mangaId, ::mapTrack)
}
}
@ -68,4 +68,34 @@ class MangaTrackRepositoryImpl(
}
}
}
private fun mapTrack(
id: Long,
mangaId: Long,
syncId: Long,
remoteId: Long,
libraryId: Long?,
title: String,
lastChapterRead: Double,
totalChapters: Long,
status: Long,
score: Double,
remoteUrl: String,
startDate: Long,
finishDate: Long,
): MangaTrack = MangaTrack(
id = id,
mangaId = mangaId,
syncId = syncId,
remoteId = remoteId,
libraryId = libraryId,
title = title,
lastChapterRead = lastChapterRead,
totalChapters = totalChapters,
status = status,
score = score,
remoteUrl = remoteUrl,
startDate = startDate,
finishDate = finishDate,
)
}

View file

@ -1,44 +0,0 @@
package tachiyomi.data.updates.anime
import tachiyomi.domain.entries.anime.model.AnimeCover
import tachiyomi.domain.updates.anime.model.AnimeUpdatesWithRelations
val animeUpdateWithRelationMapper: (
Long,
String,
Long,
String,
String?,
Boolean,
Boolean,
Long,
Long,
Long,
Boolean,
String?,
Long,
Long,
Long,
) -> AnimeUpdatesWithRelations = {
animeId, animeTitle, episodeId, episodeName, scanlator, seen, bookmark, lastSecondSeen, totalSeconds, sourceId, favorite, thumbnailUrl, coverLastModified, _, dateFetch ->
AnimeUpdatesWithRelations(
animeId = animeId,
animeTitle = animeTitle,
episodeId = episodeId,
episodeName = episodeName,
scanlator = scanlator,
seen = seen,
bookmark = bookmark,
lastSecondSeen = lastSecondSeen,
totalSeconds = totalSeconds,
sourceId = sourceId,
dateFetch = dateFetch,
coverData = AnimeCover(
animeId = animeId,
sourceId = sourceId,
isAnimeFavorite = favorite,
url = thumbnailUrl,
lastModified = coverLastModified,
),
)
}

View file

@ -2,6 +2,7 @@ package tachiyomi.data.updates.anime
import kotlinx.coroutines.flow.Flow
import tachiyomi.data.handlers.anime.AnimeDatabaseHandler
import tachiyomi.domain.entries.anime.model.AnimeCover
import tachiyomi.domain.updates.anime.model.AnimeUpdatesWithRelations
import tachiyomi.domain.updates.anime.repository.AnimeUpdatesRepository
@ -15,7 +16,7 @@ class AnimeUpdatesRepositoryImpl(
seen = seen,
after = after,
limit = limit,
mapper = animeUpdateWithRelationMapper,
mapper = ::mapUpdatesWithRelations,
)
}
}
@ -25,7 +26,7 @@ class AnimeUpdatesRepositoryImpl(
animeupdatesViewQueries.getRecentAnimeUpdates(
after,
limit,
animeUpdateWithRelationMapper,
::mapUpdatesWithRelations,
)
}
}
@ -36,8 +37,45 @@ class AnimeUpdatesRepositoryImpl(
seen = seen,
after = after,
limit = limit,
mapper = animeUpdateWithRelationMapper,
mapper = ::mapUpdatesWithRelations,
)
}
}
private fun mapUpdatesWithRelations(
animeId: Long,
animeTitle: String,
episodeId: Long,
episodeName: String,
scanlator: String?,
seen: Boolean,
bookmark: Boolean,
lastSecondSeen: Long,
totalSeconds: Long,
sourceId: Long,
favorite: Boolean,
thumbnailUrl: String?,
coverLastModified: Long,
dateUpload: Long,
dateFetch: Long,
): AnimeUpdatesWithRelations = AnimeUpdatesWithRelations(
animeId = animeId,
animeTitle = animeTitle,
episodeId = episodeId,
episodeName = episodeName,
scanlator = scanlator,
seen = seen,
bookmark = bookmark,
lastSecondSeen = lastSecondSeen,
totalSeconds = totalSeconds,
sourceId = sourceId,
dateFetch = dateFetch,
coverData = AnimeCover(
animeId = animeId,
sourceId = sourceId,
isAnimeFavorite = favorite,
url = thumbnailUrl,
lastModified = coverLastModified,
),
)
}

Some files were not shown because too many files have changed in this diff Show more