Last commit merged: d4dfa9a2c2
This commit is contained in:
LuftVerbot 2023-11-26 18:51:04 +01:00
parent 2b9fe5c389
commit 807ea3d1a5
79 changed files with 2248 additions and 698 deletions

View file

@ -20,7 +20,7 @@ android {
defaultConfig {
applicationId = "xyz.jmir.tachiyomi.mi"
versionCode = 109
versionCode = 110
versionName = "0.14.7"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
@ -172,7 +172,6 @@ dependencies {
implementation(compose.ui.util)
implementation(compose.accompanist.webview)
implementation(compose.accompanist.permissions)
implementation(compose.accompanist.themeadapter)
implementation(compose.accompanist.systemuicontroller)
lintChecks(compose.lintchecks)

View file

@ -5,21 +5,21 @@ import eu.kanade.tachiyomi.R
enum class AppTheme(val titleResId: Int?) {
DEFAULT(R.string.label_default),
MONET(R.string.theme_monet),
CLOUDFLARE(R.string.theme_cloudflare),
COTTONCANDY(R.string.theme_cottoncandy),
DOOM(R.string.theme_doom),
GREEN_APPLE(R.string.theme_greenapple),
LAVENDER(R.string.theme_lavender),
MATRIX(R.string.theme_matrix),
MIDNIGHT_DUSK(R.string.theme_midnightdusk),
MOCHA(R.string.theme_mocha),
SAPPHIRE(R.string.theme_sapphire),
STRAWBERRY_DAIQUIRI(R.string.theme_strawberrydaiquiri),
TAKO(R.string.theme_tako),
TEALTURQUOISE(R.string.theme_tealturquoise),
TIDAL_WAVE(R.string.theme_tidalwave),
YINYANG(R.string.theme_yinyang),
YOTSUBA(R.string.theme_yotsuba),
CLOUDFLARE(R.string.theme_cloudflare),
SAPPHIRE(R.string.theme_sapphire),
DOOM(R.string.theme_doom),
MATRIX(R.string.theme_matrix),
// Deprecated
DARK_BLUE(null),

View file

@ -72,7 +72,7 @@ fun AnimeExtensionScreen(
PullRefresh(
refreshing = state.isRefreshing,
onRefresh = onRefresh,
enabled = !state.isLoading,
enabled = { !state.isLoading },
) {
when {
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))

View file

@ -73,7 +73,7 @@ fun MangaExtensionScreen(
PullRefresh(
refreshing = state.isRefreshing,
onRefresh = onRefresh,
enabled = !state.isLoading,
enabled = { !state.isLoading },
) {
when {
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))

View file

@ -66,10 +66,11 @@ const val SEARCH_DEBOUNCE_MILLIS = 250L
@Composable
fun AppBar(
title: String?,
modifier: Modifier = Modifier,
backgroundColor: Color? = null,
// Text
title: String?,
subtitle: String? = null,
// Up button
navigateUp: (() -> Unit)? = null,
@ -94,7 +95,7 @@ fun AppBar(
if (isActionMode) {
AppBarTitle(actionModeCounter.toString())
} else {
AppBarTitle(title, subtitle)
AppBarTitle(title, subtitle = subtitle)
}
},
navigateUp = navigateUp,
@ -114,10 +115,11 @@ fun AppBar(
@Composable
fun AppBar(
modifier: Modifier = Modifier,
backgroundColor: Color? = null,
// Title
titleContent: @Composable () -> Unit,
modifier: Modifier = Modifier,
backgroundColor: Color? = null,
// Up button
navigateUp: (() -> Unit)? = null,
navigationIcon: ImageVector? = null,
@ -144,7 +146,7 @@ fun AppBar(
} else {
navigateUp?.let {
IconButton(onClick = it) {
UpIcon(navigationIcon)
UpIcon(navigationIcon = navigationIcon)
}
}
}
@ -164,6 +166,7 @@ fun AppBar(
@Composable
fun AppBarTitle(
title: String?,
modifier: Modifier = Modifier,
subtitle: String? = null,
count: Int = 0,
) {
@ -184,7 +187,7 @@ fun AppBarTitle(
)
}
} else {
Column {
Column(modifier = modifier) {
title?.let {
Text(
text = it,
@ -283,11 +286,12 @@ fun AppBarActions(
*/
@Composable
fun SearchToolbar(
searchQuery: String?,
onChangeSearchQuery: (String?) -> Unit,
modifier: Modifier = Modifier,
titleContent: @Composable () -> Unit = {},
navigateUp: (() -> Unit)? = null,
searchEnabled: Boolean = true,
searchQuery: String?,
onChangeSearchQuery: (String?) -> Unit,
placeholderText: String? = null,
onSearch: (String) -> Unit = {},
onClickCloseSearch: () -> Unit = { onChangeSearchQuery(null) },
@ -301,6 +305,7 @@ fun SearchToolbar(
val focusRequester = remember { FocusRequester() }
AppBar(
modifier = modifier,
titleContent = {
if (searchQuery == null) return@AppBar titleContent()
@ -422,12 +427,16 @@ fun SearchToolbar(
}
@Composable
fun UpIcon(navigationIcon: ImageVector? = null) {
fun UpIcon(
modifier: Modifier = Modifier,
navigationIcon: ImageVector? = null,
) {
val icon = navigationIcon
?: Icons.AutoMirrored.Outlined.ArrowBack
Icon(
imageVector = icon,
contentDescription = stringResource(R.string.abc_action_bar_up_description),
modifier = modifier,
)
}

View file

@ -24,6 +24,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.launch
@ -35,6 +36,7 @@ import tachiyomi.presentation.core.components.material.TabText
fun TabbedScreen(
@StringRes titleRes: Int?,
tabs: ImmutableList<TabContent>,
modifier: Modifier = Modifier,
startIndex: Int? = null,
mangaSearchQuery: String? = null,
onChangeMangaSearchQuery: (String?) -> Unit = {},
@ -42,6 +44,7 @@ fun TabbedScreen(
scrollable: Boolean = false,
animeSearchQuery: String? = null,
onChangeAnimeSearchQuery: (String?) -> Unit = {},
) {
val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
@ -69,7 +72,14 @@ fun TabbedScreen(
}
SearchToolbar(
titleContent = { AppBarTitle(stringResource(titleRes), null, tab.numberTitle) },
titleContent = {
AppBarTitle(
stringResource(titleRes),
modifier = modifier,
null,
tab.numberTitle,
)
},
searchEnabled = searchEnabled,
searchQuery = if (searchEnabled) actualQuery else null,
onChangeSearchQuery = actualOnChange,
@ -143,12 +153,14 @@ private fun FlexibleTabRow(
ScrollableTabRow(
selectedTabIndex = selectedTabIndex,
edgePadding = 13.dp,
modifier = Modifier.zIndex(1f),
) {
block()
}
} else {
PrimaryTabRow(
selectedTabIndex = selectedTabIndex,
modifier = Modifier.zIndex(1f),
) {
block()
}

View file

@ -34,10 +34,8 @@ import tachiyomi.presentation.core.theme.active
@Composable
fun EntryToolbar(
modifier: Modifier = Modifier,
title: String,
titleAlphaProvider: () -> Float,
backgroundAlphaProvider: () -> Float = titleAlphaProvider,
hasFilters: Boolean,
onBackClicked: () -> Unit,
onClickFilter: () -> Unit,
@ -54,6 +52,8 @@ fun EntryToolbar(
onSelectAll: () -> Unit,
onInvertSelection: () -> Unit,
isManga: Boolean,
modifier: Modifier = Modifier,
backgroundAlphaProvider: () -> Float = titleAlphaProvider,
) {
Column(
modifier = modifier,
@ -70,7 +70,7 @@ fun EntryToolbar(
},
navigationIcon = {
IconButton(onClick = onBackClicked) {
UpIcon(Icons.Outlined.Close.takeIf { isActionMode })
UpIcon(navigationIcon = Icons.Outlined.Close.takeIf { isActionMode })
}
},
actions = {

View file

@ -408,8 +408,8 @@ private fun AnimeScreenSmallImpl(
PullRefresh(
refreshing = state.isRefreshingData,
onRefresh = onRefresh,
enabled = !isAnySelected,
indicatorPadding = WindowInsets.systemBars.only(WindowInsetsSides.Top).asPaddingValues(),
enabled = { !isAnySelected },
indicatorPadding = PaddingValues(top = topPadding),
) {
val layoutDirection = LocalLayoutDirection.current
VerticalFastScroller(
@ -608,107 +608,102 @@ fun AnimeScreenLargeImpl(
val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
var topBarHeight by remember { mutableIntStateOf(0) }
PullRefresh(
refreshing = state.isRefreshingData,
onRefresh = onRefresh,
enabled = !isAnySelected,
indicatorPadding = PaddingValues(
start = insetPadding.calculateStartPadding(layoutDirection),
top = with(density) { topBarHeight.toDp() },
end = insetPadding.calculateEndPadding(layoutDirection),
),
) {
val episodeListState = rememberLazyListState()
val episodeListState = rememberLazyListState()
val internalOnBackPressed = {
if (isAnySelected) {
onAllEpisodeSelected(false)
} else {
onBackClicked()
}
val internalOnBackPressed = {
if (isAnySelected) {
onAllEpisodeSelected(false)
} else {
onBackClicked()
}
BackHandler(onBack = internalOnBackPressed)
}
BackHandler(onBack = internalOnBackPressed)
Scaffold(
topBar = {
val selectedEpisodeCount = remember(episodes) {
episodes.count { it.selected }
Scaffold(
topBar = {
val selectedChapterCount = remember(episodes) {
episodes.count { it.selected }
}
EntryToolbar(
modifier = Modifier.onSizeChanged { topBarHeight = it.height },
title = state.anime.title,
titleAlphaProvider = { if (isAnySelected) 1f else 0f },
backgroundAlphaProvider = { 1f },
hasFilters = state.anime.episodesFiltered(),
onBackClicked = internalOnBackPressed,
onClickFilter = onFilterButtonClicked,
onClickShare = onShareClicked,
onClickDownload = onDownloadActionClicked,
onClickEditCategory = onEditCategoryClicked,
onClickRefresh = onRefresh,
onClickMigrate = onMigrateClicked,
onClickSettings = onSettingsClicked,
changeAnimeSkipIntro = changeAnimeSkipIntro,
actionModeCounter = selectedChapterCount,
onSelectAll = { onAllEpisodeSelected(true) },
onInvertSelection = { onInvertSelection() },
isManga = false,
)
},
bottomBar = {
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.BottomEnd,
) {
val selectedEpisodes = remember(episodes) {
episodes.filter { it.selected }
}
EntryToolbar(
modifier = Modifier.onSizeChanged { topBarHeight = (it.height) },
title = state.anime.title,
titleAlphaProvider = { if (isAnySelected) 1f else 0f },
backgroundAlphaProvider = { 1f },
hasFilters = state.anime.episodesFiltered(),
onBackClicked = internalOnBackPressed,
onClickFilter = onFilterButtonClicked,
onClickShare = onShareClicked,
onClickDownload = onDownloadActionClicked,
onClickEditCategory = onEditCategoryClicked,
onClickRefresh = onRefresh,
onClickMigrate = onMigrateClicked,
onClickSettings = onSettingsClicked,
changeAnimeSkipIntro = changeAnimeSkipIntro,
actionModeCounter = selectedEpisodeCount,
onSelectAll = { onAllEpisodeSelected(true) },
onInvertSelection = { onInvertSelection() },
isManga = false,
SharedAnimeBottomActionMenu(
selected = selectedEpisodes,
onEpisodeClicked = onEpisodeClicked,
onMultiBookmarkClicked = onMultiBookmarkClicked,
onMultiMarkAsSeenClicked = onMultiMarkAsSeenClicked,
onMarkPreviousAsSeenClicked = onMarkPreviousAsSeenClicked,
onDownloadEpisode = onDownloadEpisode,
onMultiDeleteClicked = onMultiDeleteClicked,
fillFraction = 0.5f,
alwaysUseExternalPlayer = alwaysUseExternalPlayer,
)
},
bottomBar = {
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.BottomEnd,
) {
val selectedEpisodes = remember(episodes) {
episodes.filter { it.selected }
}
SharedAnimeBottomActionMenu(
selected = selectedEpisodes,
onEpisodeClicked = onEpisodeClicked,
onMultiBookmarkClicked = onMultiBookmarkClicked,
onMultiMarkAsSeenClicked = onMultiMarkAsSeenClicked,
onMarkPreviousAsSeenClicked = onMarkPreviousAsSeenClicked,
onDownloadEpisode = onDownloadEpisode,
onMultiDeleteClicked = onMultiDeleteClicked,
fillFraction = 0.5f,
alwaysUseExternalPlayer = alwaysUseExternalPlayer,
)
}
},
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
floatingActionButton = {
val isFABVisible = remember(episodes) {
episodes.fastAny { !it.episode.seen } && !isAnySelected
}
AnimatedVisibility(
visible = isFABVisible,
enter = fadeIn(),
exit = fadeOut(),
) {
ExtendedFloatingActionButton(
text = {
val isWatching = remember(state.episodes) {
state.episodes.fastAny { it.episode.seen }
}
Text(
text = stringResource(
if (isWatching) R.string.action_resume else R.string.action_start,
),
)
},
icon = {
Icon(
imageVector = Icons.Filled.PlayArrow,
contentDescription = null,
)
},
onClick = onContinueWatching,
expanded = episodeListState.isScrollingUp() || episodeListState.isScrolledToEnd(),
)
}
},
) { contentPadding ->
}
},
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
floatingActionButton = {
val isFABVisible = remember(episodes) {
episodes.fastAny { !it.episode.seen } && !isAnySelected
}
AnimatedVisibility(
visible = isFABVisible,
enter = fadeIn(),
exit = fadeOut(),
) {
ExtendedFloatingActionButton(
text = {
val isWatching = remember(state.episodes) {
state.episodes.fastAny { it.episode.seen }
}
Text(
text = stringResource(
if (isWatching) R.string.action_resume else R.string.action_start,
),
)
},
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
onClick = onContinueWatching,
expanded = episodeListState.isScrollingUp() || episodeListState.isScrolledToEnd(),
)
}
},
) { contentPadding ->
PullRefresh(
refreshing = state.isRefreshingData,
onRefresh = onRefresh,
enabled = { !isAnySelected },
indicatorPadding = PaddingValues(
start = insetPadding.calculateStartPadding(layoutDirection),
top = with(density) { topBarHeight.toDp() },
end = insetPadding.calculateEndPadding(layoutDirection),
),
) {
TwoPanelBox(
modifier = Modifier.padding(
start = contentPadding.calculateStartPadding(layoutDirection),

View file

@ -280,13 +280,12 @@ private fun MangaScreenSmallImpl(
) {
val chapterListState = rememberLazyListState()
val chapters = remember(state) { state.processedChapters }
val listItem = remember(state) { state.chapterListItems }
val isAnySelected by remember {
derivedStateOf {
chapters.fastAny { it.selected }
}
val (chapters, listItem, isAnySelected) = remember(state) {
Triple(
first = state.processedChapters,
second = state.chapterListItems,
third = state.isAnySelected,
)
}
val internalOnBackPressed = {
@ -380,8 +379,8 @@ private fun MangaScreenSmallImpl(
PullRefresh(
refreshing = state.isRefreshingData,
onRefresh = onRefresh,
enabled = !isAnySelected,
indicatorPadding = WindowInsets.systemBars.only(WindowInsetsSides.Top).asPaddingValues(),
enabled = { !isAnySelected },
indicatorPadding = PaddingValues(top = topPadding),
) {
val layoutDirection = LocalLayoutDirection.current
VerticalFastScroller(
@ -537,114 +536,111 @@ fun MangaScreenLargeImpl(
val layoutDirection = LocalLayoutDirection.current
val density = LocalDensity.current
val chapters = remember(state) { state.processedChapters }
val listItem = remember(state) { state.chapterListItems }
val isAnySelected by remember {
derivedStateOf {
chapters.fastAny { it.selected }
}
val (chapters, listItem, isAnySelected) = remember(state) {
Triple(
first = state.processedChapters,
second = state.chapterListItems,
third = state.isAnySelected,
)
}
val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
var topBarHeight by remember { mutableIntStateOf(0) }
PullRefresh(
refreshing = state.isRefreshingData,
onRefresh = onRefresh,
enabled = !isAnySelected,
indicatorPadding = PaddingValues(
start = insetPadding.calculateStartPadding(layoutDirection),
top = with(density) { topBarHeight.toDp() },
end = insetPadding.calculateEndPadding(layoutDirection),
),
) {
val chapterListState = rememberLazyListState()
val internalOnBackPressed = {
if (isAnySelected) {
onAllChapterSelected(false)
} else {
onBackClicked()
}
val chapterListState = rememberLazyListState()
val internalOnBackPressed = {
if (isAnySelected) {
onAllChapterSelected(false)
} else {
onBackClicked()
}
BackHandler(onBack = internalOnBackPressed)
}
BackHandler(onBack = internalOnBackPressed)
Scaffold(
topBar = {
val selectedChapterCount = remember(chapters) {
chapters.count { it.selected }
Scaffold(
topBar = {
val selectedChapterCount = remember(chapters) {
chapters.count { it.selected }
}
EntryToolbar(
modifier = Modifier.onSizeChanged { topBarHeight = it.height },
title = state.manga.title,
titleAlphaProvider = { if (isAnySelected) 1f else 0f },
backgroundAlphaProvider = { 1f },
hasFilters = state.filterActive,
onBackClicked = internalOnBackPressed,
onClickFilter = onFilterButtonClicked,
onClickShare = onShareClicked,
onClickDownload = onDownloadActionClicked,
onClickEditCategory = onEditCategoryClicked,
onClickRefresh = onRefresh,
onClickMigrate = onMigrateClicked,
onClickSettings = onSettingsClicked,
changeAnimeSkipIntro = null,
actionModeCounter = selectedChapterCount,
onSelectAll = { onAllChapterSelected(true) },
onInvertSelection = { onInvertSelection() },
isManga = true,
)
},
bottomBar = {
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.BottomEnd,
) {
val selectedChapters = remember(chapters) {
chapters.filter { it.selected }
}
EntryToolbar(
modifier = Modifier.onSizeChanged { topBarHeight = it.height },
title = state.manga.title,
titleAlphaProvider = { if (isAnySelected) 1f else 0f },
backgroundAlphaProvider = { 1f },
hasFilters = state.filterActive,
onBackClicked = internalOnBackPressed,
onClickFilter = onFilterButtonClicked,
onClickShare = onShareClicked,
onClickDownload = onDownloadActionClicked,
onClickEditCategory = onEditCategoryClicked,
onClickRefresh = onRefresh,
onClickMigrate = onMigrateClicked,
onClickSettings = onSettingsClicked,
changeAnimeSkipIntro = null,
actionModeCounter = selectedChapterCount,
onSelectAll = { onAllChapterSelected(true) },
onInvertSelection = { onInvertSelection() },
isManga = true,
SharedMangaBottomActionMenu(
selected = selectedChapters,
onMultiBookmarkClicked = onMultiBookmarkClicked,
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
onDownloadChapter = onDownloadChapter,
onMultiDeleteClicked = onMultiDeleteClicked,
fillFraction = 0.5f,
)
},
bottomBar = {
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.BottomEnd,
) {
val selectedChapters = remember(chapters) {
chapters.filter { it.selected }
}
SharedMangaBottomActionMenu(
selected = selectedChapters,
onMultiBookmarkClicked = onMultiBookmarkClicked,
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
onDownloadChapter = onDownloadChapter,
onMultiDeleteClicked = onMultiDeleteClicked,
fillFraction = 0.5f,
)
}
},
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
floatingActionButton = {
val isFABVisible = remember(chapters) {
chapters.fastAny { !it.chapter.read } && !isAnySelected
}
AnimatedVisibility(
visible = isFABVisible,
enter = fadeIn(),
exit = fadeOut(),
) {
ExtendedFloatingActionButton(
text = {
val isReading = remember(state.chapters) {
state.chapters.fastAny { it.chapter.read }
}
Text(
text = stringResource(if (isReading) R.string.action_resume else R.string.action_start),
)
},
icon = {
Icon(
imageVector = Icons.Filled.PlayArrow,
contentDescription = null,
)
},
onClick = onContinueReading,
expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(),
)
}
},
) { contentPadding ->
}
},
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
floatingActionButton = {
val isFABVisible = remember(chapters) {
chapters.fastAny { !it.chapter.read } && !isAnySelected
}
AnimatedVisibility(
visible = isFABVisible,
enter = fadeIn(),
exit = fadeOut(),
) {
ExtendedFloatingActionButton(
text = {
val isReading = remember(state.chapters) {
state.chapters.fastAny { it.chapter.read }
}
Text(
text = stringResource(
if (isReading) R.string.action_resume else R.string.action_start,
),
)
},
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
onClick = onContinueReading,
expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(),
)
}
},
) { contentPadding ->
PullRefresh(
refreshing = state.isRefreshingData,
onRefresh = onRefresh,
enabled = { !isAnySelected },
indicatorPadding = PaddingValues(
start = insetPadding.calculateStartPadding(layoutDirection),
top = with(density) { topBarHeight.toDp() },
end = insetPadding.calculateEndPadding(layoutDirection),
),
) {
TwoPanelBox(
modifier = Modifier.padding(
start = contentPadding.calculateStartPadding(layoutDirection),
@ -743,13 +739,13 @@ fun MangaScreenLargeImpl(
@Composable
private fun SharedMangaBottomActionMenu(
selected: List<ChapterList.Item>,
modifier: Modifier = Modifier,
onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit,
onMarkPreviousAsReadClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onMultiDeleteClicked: (List<Chapter>) -> Unit,
fillFraction: Float,
modifier: Modifier = Modifier,
) {
EntryBottomActionMenu(
visible = selected.isNotEmpty(),

View file

@ -51,7 +51,7 @@ object CommonEntryItemDefaults {
const val BrowseFavoriteCoverAlpha = 0.34f
}
private val ContinueViewingButtonSize = 32.dp
private val ContinueViewingButtonSize = 28.dp
private val ContinueViewingButtonGridPadding = 6.dp
private val ContinueViewingButtonListSpacing = 8.dp

View file

@ -7,7 +7,9 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.Tab
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import eu.kanade.presentation.category.visualName
import tachiyomi.domain.category.model.Category
import tachiyomi.presentation.core.components.material.TabText
@ -19,7 +21,9 @@ fun LibraryTabs(
getNumberOfItemsForCategory: (Category) -> Int?,
onTabItemClick: (Int) -> Unit,
) {
Column {
Column(
modifier = Modifier.zIndex(1f),
) {
PrimaryScrollableTabRow(
selectedTabIndex = pagerState.currentPage,
edgePadding = 0.dp,

View file

@ -94,7 +94,7 @@ fun AnimeLibraryContent(
isRefreshing = false
}
},
enabled = notSelectionMode,
enabled = { notSelectionMode },
) {
AnimeLibraryPager(
state = pagerState,

View file

@ -94,7 +94,7 @@ fun MangaLibraryContent(
isRefreshing = false
}
},
enabled = notSelectionMode,
enabled = { notSelectionMode },
) {
MangaLibraryPager(
state = pagerState,

View file

@ -1,5 +1,6 @@
package eu.kanade.presentation.more.settings.screen
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
@ -28,13 +29,16 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.core.net.toUri
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import com.hippo.unifile.UniFile
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen
import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
import eu.kanade.presentation.permissions.PermissionRequestHelper
import eu.kanade.presentation.util.relativeTimeSpanString
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupCreateJob
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
@ -58,6 +62,7 @@ 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.domain.storage.service.StoragePreferences
import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -72,20 +77,59 @@ object SettingsDataScreen : SearchableSettings {
@Composable
override fun getPreferences(): List<Preference> {
val backupPreferences = Injekt.get<BackupPreferences>()
val storagePreferences = Injekt.get<StoragePreferences>()
PermissionRequestHelper.requestStoragePermission()
return listOf(
getStorageLocationPref(storagePreferences = storagePreferences),
Preference.PreferenceItem.InfoPreference(stringResource(R.string.pref_storage_location_info)),
getBackupAndRestoreGroup(backupPreferences = backupPreferences),
getDataGroup(backupPreferences = backupPreferences),
)
}
@Composable
private fun getStorageLocationPref(
storagePreferences: StoragePreferences,
): Preference.PreferenceItem.TextPreference {
val context = LocalContext.current
val storageDirPref = storagePreferences.baseStorageDirectory()
val storageDir by storageDirPref.collectAsState()
val pickStorageLocation = 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)
storageDirPref.set(file.uri.toString())
}
}
return Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_storage_location),
subtitle = remember(storageDir) {
(UniFile.fromUri(context, storageDir.toUri())?.filePath)
} ?: stringResource(R.string.invalid_location, storageDir),
onClick = {
try {
pickStorageLocation.launch(null)
} catch (e: ActivityNotFoundException) {
context.toast(R.string.file_picker_error)
}
},
)
}
@Composable
private fun getBackupAndRestoreGroup(backupPreferences: BackupPreferences): Preference.PreferenceGroup {
val context = LocalContext.current
val backupIntervalPref = backupPreferences.backupInterval()
val backupInterval by backupIntervalPref.collectAsState()
val lastAutoBackup by backupPreferences.lastAutoBackupTimestamp().collectAsState()
return Preference.PreferenceGroup(
title = stringResource(R.string.label_backup),
@ -96,7 +140,7 @@ object SettingsDataScreen : SearchableSettings {
// Automatic backups
Preference.PreferenceItem.ListPreference(
pref = backupIntervalPref,
pref = backupPreferences.backupInterval(),
title = stringResource(R.string.pref_backup_interval),
entries = mapOf(
0 to stringResource(R.string.off),
@ -111,13 +155,10 @@ object SettingsDataScreen : SearchableSettings {
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) + "\n\n" +
stringResource(R.string.last_auto_backup_info, relativeTimeSpanString(lastAutoBackup)),
),
Preference.PreferenceItem.InfoPreference(stringResource(R.string.backup_info)),
),
)
}

View file

@ -1,9 +1,5 @@
package eu.kanade.presentation.more.settings.screen
import android.content.Intent
import android.os.Environment
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
@ -13,12 +9,9 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.util.fastMap
import androidx.core.net.toUri
import com.hippo.unifile.UniFile
import eu.kanade.domain.base.BasePreferences
import eu.kanade.presentation.category.visualName
import eu.kanade.presentation.more.settings.Preference
@ -32,7 +25,6 @@ import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
object SettingsDownloadScreen : SearchableSettings {
@ -56,7 +48,6 @@ object SettingsDownloadScreen : SearchableSettings {
val basePreferences = remember { Injekt.get<BasePreferences>() }
return listOf(
getDownloadLocationPreference(downloadPreferences = downloadPreferences),
Preference.PreferenceItem.SwitchPreference(
pref = downloadPreferences.downloadOnlyOverWifi(),
title = stringResource(R.string.connected_to_wifi),
@ -93,84 +84,6 @@ object SettingsDownloadScreen : SearchableSettings {
)
}
@Composable
private fun getDownloadLocationPreference(
downloadPreferences: DownloadPreferences,
): Preference.PreferenceItem.ListPreference<String> {
val context = LocalContext.current
val currentDirPref = downloadPreferences.downloadsDirectory()
val currentDir by currentDirPref.collectAsState()
val pickLocation = 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)
currentDirPref.set(file.uri.toString())
}
}
val defaultDirPair = rememberDefaultDownloadDir()
val externalDownloaderDirPair = rememberExternalDownloaderDownloadDir()
val customDirEntryKey = currentDir.takeIf { it != defaultDirPair.first } ?: "custom"
return Preference.PreferenceItem.ListPreference(
pref = currentDirPref,
title = stringResource(R.string.pref_download_directory),
subtitleProvider = { value, _ ->
remember(value) {
UniFile.fromUri(context, value.toUri())?.filePath
} ?: stringResource(R.string.invalid_location, value)
},
entries = mapOf(
defaultDirPair,
externalDownloaderDirPair,
customDirEntryKey to stringResource(R.string.custom_dir),
),
onValueChanged = {
val default = it == defaultDirPair.first
if (!default) {
pickLocation.launch(null)
}
default // Don't update when non-default chosen
},
)
}
@Composable
private fun rememberDefaultDownloadDir(): Pair<String, String> {
val appName = stringResource(R.string.app_name)
return remember {
val file = UniFile.fromFile(
File(
"${Environment.getExternalStorageDirectory().absolutePath}${File.separator}$appName",
"downloads",
),
)!!
file.uri.toString() to file.filePath!!
}
}
@Composable
private fun rememberExternalDownloaderDownloadDir(): Pair<String, String> {
val appName = stringResource(R.string.app_name)
return remember {
val file = UniFile.fromFile(
File(
Environment.getExternalStorageDirectory().absolutePath +
"${File.separator}${Environment.DIRECTORY_DOWNLOADS}${File.separator}$appName",
"downloads",
),
)!!
"(ADM)" + file.uri.toString() to file.filePath!!
}
}
@Composable
private fun getDeleteChaptersGroup(
downloadPreferences: DownloadPreferences,

View file

@ -257,10 +257,12 @@ object SettingsReaderScreen : SearchableSettings {
val navModePref = readerPreferences.navigationModeWebtoon()
val dualPageSplitPref = readerPreferences.dualPageSplitWebtoon()
val rotateToFitPref = readerPreferences.dualPageRotateToFitWebtoon()
val webtoonSidePaddingPref = readerPreferences.webtoonSidePadding()
val navMode by navModePref.collectAsState()
val dualPageSplit by dualPageSplitPref.collectAsState()
val rotateToFit by rotateToFitPref.collectAsState()
val webtoonSidePadding by webtoonSidePaddingPref.collectAsState()
return Preference.PreferenceGroup(
@ -326,6 +328,10 @@ object SettingsReaderScreen : SearchableSettings {
Preference.PreferenceItem.SwitchPreference(
pref = dualPageSplitPref,
title = stringResource(R.string.pref_dual_page_split),
onValueChanged = {
rotateToFitPref.set(false)
true
},
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.dualPageInvertWebtoon(),
@ -333,6 +339,19 @@ object SettingsReaderScreen : SearchableSettings {
subtitle = stringResource(R.string.pref_dual_page_invert_summary),
enabled = dualPageSplit,
),
Preference.PreferenceItem.SwitchPreference(
pref = rotateToFitPref,
title = stringResource(R.string.pref_page_rotate),
onValueChanged = {
dualPageSplitPref.set(false)
true
},
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.dualPageRotateToFitInvertWebtoon(),
title = stringResource(R.string.pref_page_rotate_invert),
enabled = rotateToFit,
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.webtoonDoubleTapZoomEnabled(),
title = stringResource(R.string.pref_double_tap_zoom),

View file

@ -188,6 +188,19 @@ private fun ColumnScope.WebtoonViewerSettings(screenModel: ReaderSettingsScreenM
)
}
val dualPageRotateToFitWebtoon by screenModel.preferences.dualPageRotateToFitWebtoon().collectAsState()
CheckboxItem(
label = stringResource(R.string.pref_page_rotate),
pref = screenModel.preferences.dualPageRotateToFitWebtoon(),
)
if (dualPageRotateToFitWebtoon) {
CheckboxItem(
label = stringResource(R.string.pref_page_rotate_invert),
pref = screenModel.preferences.dualPageRotateToFitInvertWebtoon(),
)
}
CheckboxItem(
label = stringResource(R.string.pref_double_tap_zoom),
pref = screenModel.preferences.webtoonDoubleTapZoomEnabled(),

View file

@ -1,54 +1,74 @@
package eu.kanade.presentation.theme
import androidx.appcompat.view.ContextThemeWrapper
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import com.google.accompanist.themeadapter.material3.createMdc3Theme
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.domain.ui.model.AppTheme
import eu.kanade.tachiyomi.ui.base.delegate.ThemingDelegate
@Composable
fun TachiyomiTheme(content: @Composable () -> Unit) {
val context = LocalContext.current
val layoutDirection = LocalLayoutDirection.current
val (colorScheme, typography) = createMdc3Theme(
context = context,
layoutDirection = layoutDirection,
)
MaterialTheme(
colorScheme = colorScheme!!,
typography = typography!!,
content = content,
)
}
import eu.kanade.presentation.theme.colorscheme.CloudflareColorScheme
import eu.kanade.presentation.theme.colorscheme.CottoncandyColorScheme
import eu.kanade.presentation.theme.colorscheme.DoomColorScheme
import eu.kanade.presentation.theme.colorscheme.GreenAppleColorScheme
import eu.kanade.presentation.theme.colorscheme.LavenderColorScheme
import eu.kanade.presentation.theme.colorscheme.MatrixColorScheme
import eu.kanade.presentation.theme.colorscheme.MidnightDuskColorScheme
import eu.kanade.presentation.theme.colorscheme.MochaColorScheme
import eu.kanade.presentation.theme.colorscheme.MonetColorScheme
import eu.kanade.presentation.theme.colorscheme.SapphireColorScheme
import eu.kanade.presentation.theme.colorscheme.StrawberryColorScheme
import eu.kanade.presentation.theme.colorscheme.TachiyomiColorScheme
import eu.kanade.presentation.theme.colorscheme.TakoColorScheme
import eu.kanade.presentation.theme.colorscheme.TealTurqoiseColorScheme
import eu.kanade.presentation.theme.colorscheme.TidalWaveColorScheme
import eu.kanade.presentation.theme.colorscheme.YinYangColorScheme
import eu.kanade.presentation.theme.colorscheme.YotsubaColorScheme
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Composable
fun TachiyomiTheme(
appTheme: AppTheme,
amoled: Boolean,
appTheme: AppTheme? = null,
amoled: Boolean? = null,
content: @Composable () -> Unit,
) {
val originalContext = LocalContext.current
val layoutDirection = LocalLayoutDirection.current
val themedContext = remember(appTheme, originalContext) {
val themeResIds = ThemingDelegate.getThemeResIds(appTheme, amoled)
themeResIds.fold(originalContext) { context, themeResId ->
ContextThemeWrapper(context, themeResId)
}
}
val (colorScheme, typography) = createMdc3Theme(
context = themedContext,
layoutDirection = layoutDirection,
)
MaterialTheme(
colorScheme = colorScheme!!,
typography = typography!!,
colorScheme = getThemeColorScheme(appTheme, amoled),
content = content,
)
}
@Composable
@ReadOnlyComposable
private fun getThemeColorScheme(
appTheme: AppTheme?,
amoled: Boolean?,
): ColorScheme {
val uiPreferences = Injekt.get<UiPreferences>()
val colorScheme = when (appTheme ?: uiPreferences.appTheme().get()) {
AppTheme.DEFAULT -> TachiyomiColorScheme
AppTheme.MONET -> MonetColorScheme(LocalContext.current)
AppTheme.CLOUDFLARE -> CloudflareColorScheme
AppTheme.COTTONCANDY -> CottoncandyColorScheme
AppTheme.DOOM -> DoomColorScheme
AppTheme.GREEN_APPLE -> GreenAppleColorScheme
AppTheme.LAVENDER -> LavenderColorScheme
AppTheme.MATRIX -> MatrixColorScheme
AppTheme.MIDNIGHT_DUSK -> MidnightDuskColorScheme
AppTheme.MOCHA -> MochaColorScheme
AppTheme.SAPPHIRE -> SapphireColorScheme
AppTheme.STRAWBERRY_DAIQUIRI -> StrawberryColorScheme
AppTheme.TAKO -> TakoColorScheme
AppTheme.TEALTURQUOISE -> TealTurqoiseColorScheme
AppTheme.TIDAL_WAVE -> TidalWaveColorScheme
AppTheme.YINYANG -> YinYangColorScheme
AppTheme.YOTSUBA -> YotsubaColorScheme
else -> TachiyomiColorScheme
}
return colorScheme.getColorScheme(
isSystemInDarkTheme(),
amoled ?: uiPreferences.themeDarkAmoled().get(),
)
}

View file

@ -0,0 +1,26 @@
package eu.kanade.presentation.theme.colorscheme
import androidx.compose.material3.ColorScheme
import androidx.compose.ui.graphics.Color
internal abstract class BaseColorScheme {
abstract val darkScheme: ColorScheme
abstract val lightScheme: ColorScheme
fun getColorScheme(isDark: Boolean, isAmoled: Boolean): ColorScheme {
return (if (isDark) darkScheme else lightScheme)
.let {
if (isDark && isAmoled) {
it.copy(
background = Color.Black,
onBackground = Color.White,
surface = Color.Black,
onSurface = Color.White,
)
} else {
it
}
}
}
}

View file

@ -0,0 +1,71 @@
package eu.kanade.presentation.theme.colorscheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
/**
* Colors for Clouflare theme
* Original color scheme by LuftVerbot
* M3 colors generated by Material Theme Builder (https://goo.gle/material-theme-builder-web)
*
* Key colors:
* Primary 0xFFF38020
* Secondary 0xFFF38020
* Tertiary 0xFF1B1B22
* Neutral 0xFF655C5A
*/
internal object CloudflareColorScheme : BaseColorScheme() {
override val darkScheme = darkColorScheme(
primary = Color(0xFFF38020),
onPrimary = Color(0xFF1B1B22),
primaryContainer = Color(0xFFF38020),
onPrimaryContainer = Color(0xFF1B1B22),
inversePrimary = Color(0xFFD6BAFF), // Assuming 'inversePrimary' maps to 'cloudflare_primaryInverse'
secondary = Color(0xFFF38020),
onSecondary = Color(0xFF1B1B22),
secondaryContainer = Color(0xFFF38020),
onSecondaryContainer = Color(0xFF1B1B22),
tertiary = Color(0xFF1B1B22),
onTertiary = Color(0xFFF38020),
tertiaryContainer = Color(0xFF1B1B22),
onTertiaryContainer = Color(0xFFF38020),
background = Color(0xFF1B1B22),
onBackground = Color(0xFFEFF2F5),
surface = Color(0xFF1B1B22),
onSurface = Color(0xFFEFF2F5),
surfaceVariant = Color(0xFF3F3F46),
onSurfaceVariant = Color(0xFFD8FFFFFF),
surfaceTint = Color(0xFFF38020), // Assuming 'surfaceTint' maps to 'cloudflare_primary' or similar
inverseSurface = Color(0xFFF3EFF4),
inverseOnSurface = Color(0xFF313033),
outline = Color(0xFFF38020),
)
override val lightScheme = lightColorScheme(
primary = Color(0xFFF38020),
onPrimary = Color(0xFFEFF2F5),
primaryContainer = Color(0xFFF38020),
onPrimaryContainer = Color(0xFFEFF2F5),
inversePrimary = Color(0xFFD6BAFF), // Assuming 'inversePrimary' maps to 'cloudflare_primaryInverse'
secondary = Color(0xFFF38020),
onSecondary = Color(0xFFEFF2F5),
secondaryContainer = Color(0xFFF38020),
onSecondaryContainer = Color(0xFFEFF2F5),
tertiary = Color(0xFFEFF2F5),
onTertiary = Color(0xFFF38020),
tertiaryContainer = Color(0xFFEFF2F5),
onTertiaryContainer = Color(0xFFF38020),
background = Color(0xFFEFF2F5),
onBackground = Color(0xFF1B1B22),
surface = Color(0xFFEFF2F5),
onSurface = Color(0xFF1B1B22),
surfaceVariant = Color(0xFFB9B0CC),
onSurfaceVariant = Color(0xFFD849454E),
surfaceTint = Color(0xFFF38020), // Assuming 'surfaceTint' maps to 'cloudflare_primary' or similar
inverseSurface = Color(0xFF313033),
inverseOnSurface = Color(0xFFF3EFF4),
outline = Color(0xFFF38020),
)
}

View file

@ -0,0 +1,70 @@
package eu.kanade.presentation.theme.colorscheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
/**
* Colors for Cottoncandy theme
* M3 colors generated by Material Theme Builder (https://goo.gle/material-theme-builder-web)
*
* Key colors:
* Primary 0xFFF38020
* Secondary 0xFFF38020
* Tertiary 0xFF1B1B22
* Neutral 0xFF655C5A
*/
internal object CottoncandyColorScheme : BaseColorScheme() {
override val darkScheme = darkColorScheme(
primary = Color(0xFFFFB1C1),
onPrimary = Color(0xFF5F112B),
primaryContainer = Color(0xFF7C2941),
onPrimaryContainer = Color(0xFFFFD9DF),
secondary = Color(0xFF64D3FF),
onSecondary = Color(0xFF003546),
secondaryContainer = Color(0xFF004D63),
onSecondaryContainer = Color(0xFFBCE9FF),
tertiary = Color(0xFFFFB1C1),
onTertiary = Color(0xFF5F112B), // Note: onTertiary color is assumed
tertiaryContainer = Color(0xFF7C2941),
onTertiaryContainer = Color(0xFFFFD9DF),
background = Color(0xFF201A1B),
onBackground = Color(0xFFECE0E0),
surface = Color(0xFF201A1B),
onSurface = Color(0xFFECE0E0),
surfaceVariant = Color(0xFF524345),
onSurfaceVariant = Color(0xFFD6C2C4),
surfaceTint = Color(0xFFFFB1C1),
inverseSurface = Color(0xFFECE0E0),
inverseOnSurface = Color(0xFF201A1B),
outline = Color(0xFF9F8C8F),
inversePrimary = Color(0xFF9A4058),
)
override val lightScheme = lightColorScheme(
primary = Color(0xFF9A4058),
onPrimary = Color(0xFFFFFFFF),
primaryContainer = Color(0xFFFFD9DF),
onPrimaryContainer = Color(0xFF3F0017),
secondary = Color(0xFF5BCEFA),
onSecondary = Color(0xFFFFFFFF),
secondaryContainer = Color(0xFFBCE9FF),
onSecondaryContainer = Color(0xFF001F2A),
tertiary = Color(0xFF9A4058),
onTertiary = Color(0xFFFFFFFF),
tertiaryContainer = Color(0xFFFFD9DF),
onTertiaryContainer = Color(0xFF3F0017),
background = Color(0xFFFFFBFF),
onBackground = Color(0xFF201A1B),
surface = Color(0xFFFFFBFF),
onSurface = Color(0xFF201A1B),
surfaceVariant = Color(0xFFF3DDE0),
onSurfaceVariant = Color(0xFF524345),
surfaceTint = Color(0xFF9A4058),
inverseSurface = Color(0xFF352F30),
inverseOnSurface = Color(0xFFFAEEEF),
outline = Color(0xFF847375),
inversePrimary = Color(0xFFFFB1C1),
)
}

View file

@ -0,0 +1,71 @@
package eu.kanade.presentation.theme.colorscheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
/**
* Colors for Doom theme
* Original color scheme by LuftVerbot
* M3 colors generated by Material Theme Builder (https://goo.gle/material-theme-builder-web)
*
* Key colors:
* Primary 0xFFF38020
* Secondary 0xFFF38020
* Tertiary 0xFF1B1B22
* Neutral 0xFF655C5A
*/
internal object DoomColorScheme : BaseColorScheme() {
override val darkScheme = darkColorScheme(
primary = Color(0xFFFF0000),
onPrimary = Color(0xFFFAFAFA),
primaryContainer = Color(0xFFFF0000),
onPrimaryContainer = Color(0xFFFAFAFA),
secondary = Color(0xFFFF0000),
onSecondary = Color(0xFFFAFAFA),
secondaryContainer = Color(0xFFFF0000),
onSecondaryContainer = Color(0xFFFAFAFA),
tertiary = Color(0xFFBFBFBF),
onTertiary = Color(0xFFFF0000),
tertiaryContainer = Color(0xFFBFBFBF),
onTertiaryContainer = Color(0xFFFF0000),
background = Color(0xFF1B1B1B),
onBackground = Color(0xFFFFFFFF),
surface = Color(0xFF1B1B1B),
onSurface = Color(0xFFFFFFFF),
surfaceVariant = Color(0xFF303030),
onSurfaceVariant = Color(0xFFD8FFFFFF),
surfaceTint = Color(0xFFFF0000),
inverseSurface = Color(0xFFFAFAFA),
inverseOnSurface = Color(0xFF313131),
outline = Color(0xFFFF0000),
inversePrimary = Color(0xFF6D0D0B),
)
override val lightScheme = lightColorScheme(
primary = Color(0xFFFF0000),
onPrimary = Color(0xFFFFFFFF),
primaryContainer = Color(0xFFFF0000),
onPrimaryContainer = Color(0xFFFFFFFF),
inversePrimary = Color(0xFF6D0D0B), // Assuming 'inversePrimary' maps to 'doom_primaryInverse'
secondary = Color(0xFFFF0000),
onSecondary = Color(0xFFFFFFFF),
secondaryContainer = Color(0xFFFF0000),
onSecondaryContainer = Color(0xFFFFFFFF),
tertiary = Color(0xFFBFBFBF),
onTertiary = Color(0xFFFF0000),
tertiaryContainer = Color(0xFFBFBFBF),
onTertiaryContainer = Color(0xFFFF0000),
background = Color(0xFF212121),
onBackground = Color(0xFFFFFFFF),
surface = Color(0xFF212121),
onSurface = Color(0xFFFFFFFF),
surfaceVariant = Color(0xFF4D4D4D),
onSurfaceVariant = Color(0xFFD849454E),
surfaceTint = Color(0xFFFF0000), // Assuming 'surfaceTint' maps to 'doom_primary' or similar
inverseSurface = Color(0xFF424242),
inverseOnSurface = Color(0xFFFAFAFA),
outline = Color(0xFFFF0000),
)
}

View file

@ -0,0 +1,71 @@
package eu.kanade.presentation.theme.colorscheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
/**
* Colors for Green Apple theme
* Original color scheme by CarlosEsco, Jays2Kings and CrepeTF
* M3 colors generated by Material Theme Builder (https://goo.gle/material-theme-builder-web)
*
* Key colors:
* Primary #188140
* Secondary #188140
* Tertiary #D33131
* Neutral #5D5F5B
*/
internal object GreenAppleColorScheme : BaseColorScheme() {
override val darkScheme = darkColorScheme(
primary = Color(0xFF7ADB8F),
onPrimary = Color(0xFF003915),
primaryContainer = Color(0xFF005322),
onPrimaryContainer = Color(0xFF96F8A9),
inversePrimary = Color(0xFF006D2F),
secondary = Color(0xFF7ADB8F),
onSecondary = Color(0xFF003915),
secondaryContainer = Color(0xFF005322),
onSecondaryContainer = Color(0xFF96F8A9),
tertiary = Color(0xFFFFB3AA),
onTertiary = Color(0xFF680006),
tertiaryContainer = Color(0xFF93000D),
onTertiaryContainer = Color(0xFFFFDAD5),
background = Color(0xFF1A1C19),
onBackground = Color(0xFFE1E3DD),
surface = Color(0xFF1A1C19),
onSurface = Color(0xFFE1E3DD),
surfaceVariant = Color(0xFF414941),
onSurfaceVariant = Color(0xFFC1C8BE),
surfaceTint = Color(0xFF7ADB8F),
inverseSurface = Color(0xFFE1E3DD),
inverseOnSurface = Color(0xFF1A1C19),
outline = Color(0xFF8B9389),
)
override val lightScheme = lightColorScheme(
primary = Color(0xFF006D2F),
onPrimary = Color(0xFFFFFFFF),
primaryContainer = Color(0xFF96F8A9),
onPrimaryContainer = Color(0xFF002109),
inversePrimary = Color(0xFF7ADB8F),
secondary = Color(0xFF006D2F),
onSecondary = Color(0xFFFFFFFF),
secondaryContainer = Color(0xFF96F8A9),
onSecondaryContainer = Color(0xFF002109),
tertiary = Color(0xFFB91D22),
onTertiary = Color(0xFFFFFFFF),
tertiaryContainer = Color(0xFFFFDAD5),
onTertiaryContainer = Color(0xFF410003),
background = Color(0xFFFBFDF7),
onBackground = Color(0xFF1A1C19),
surface = Color(0xFFFBFDF7),
onSurface = Color(0xFF1A1C19),
surfaceVariant = Color(0xFFDDE5DA),
onSurfaceVariant = Color(0xFF414941),
surfaceTint = Color(0xFF006D2F),
inverseSurface = Color(0xFF2F312E),
inverseOnSurface = Color(0xFFF0F2EC),
outline = Color(0xFF717970),
)
}

View file

@ -0,0 +1,70 @@
package eu.kanade.presentation.theme.colorscheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
/**
* Colors for Lavender theme
* Color scheme by Osyx
*
* Key colors:
* Primary #A177FF
* Secondary #A177FF
* Tertiary #5E25E1
* Neutral #111129
*/
internal object LavenderColorScheme : BaseColorScheme() {
override val darkScheme = darkColorScheme(
primary = Color(0xFFA177FF),
onPrimary = Color(0xFF111129),
primaryContainer = Color(0xFFA177FF),
onPrimaryContainer = Color(0xFF111129),
inversePrimary = Color(0xFF006D2F),
secondary = Color(0xFFA177FF),
onSecondary = Color(0xFF111129),
secondaryContainer = Color(0xFFA177FF),
onSecondaryContainer = Color(0xFF111129),
tertiary = Color(0xFF5E25E1),
onTertiary = Color(0xFFE8E8E8),
tertiaryContainer = Color(0xFF111129),
onTertiaryContainer = Color(0xFFDEE8FF),
background = Color(0xFF111129),
onBackground = Color(0xFFDEE8FF),
surface = Color(0xFF111129),
onSurface = Color(0xFFDEE8FF),
surfaceVariant = Color(0x2CB6B6B6),
onSurfaceVariant = Color(0xFFE8E8E8),
surfaceTint = Color(0xFFA177FF),
inverseSurface = Color(0xFF221247),
inverseOnSurface = Color(0xFFDEE8FF),
outline = Color(0xA8905FFF),
)
override val lightScheme = lightColorScheme(
primary = Color(0xFF7B46AF),
onPrimary = Color(0xFFEDE2FF),
primaryContainer = Color(0xFF7B46AF),
onPrimaryContainer = Color(0xFFEDE2FF),
inversePrimary = Color(0xFFD6BAFF),
secondary = Color(0xFF7B46AF),
onSecondary = Color(0xFFEDE2FF),
secondaryContainer = Color(0xFF7B46AF),
onSecondaryContainer = Color(0xFFEDE2FF),
tertiary = Color(0xFFEDE2FF),
onTertiary = Color(0xFF7B46AF),
tertiaryContainer = Color(0xFFEDE2FF),
onTertiaryContainer = Color(0xFF7B46AF),
background = Color(0xFFEDE2FF),
onBackground = Color(0xFF1B1B22),
surface = Color(0xFFEDE2FF),
onSurface = Color(0xFF1B1B22),
surfaceVariant = Color(0xFFB9B0CC),
onSurfaceVariant = Color(0xD849454E),
surfaceTint = Color(0xFF7B46AF),
inverseSurface = Color(0xFF313033),
inverseOnSurface = Color(0xFFF3EFF4),
outline = Color(0xFF7B46AF),
)
}

View file

@ -0,0 +1,71 @@
package eu.kanade.presentation.theme.colorscheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
/**
* Colors for Matrix theme
* Original color scheme by LuftVerbot
* M3 colors generated by Material Theme Builder (https://goo.gle/material-theme-builder-web)
*
* Key colors:
* Primary 0xFFF38020
* Secondary 0xFFF38020
* Tertiary 0xFF1B1B22
* Neutral 0xFF655C5A
*/
internal object MatrixColorScheme : BaseColorScheme() {
override val darkScheme = darkColorScheme(
primary = Color(0xFF00FF00),
onPrimary = Color(0xFFFAFAFA),
primaryContainer = Color(0xFF00FF00),
onPrimaryContainer = Color(0xFFFAFAFA),
secondary = Color(0xFF00FF00),
onSecondary = Color(0xFFFAFAFA),
secondaryContainer = Color(0xFF00FF00),
onSecondaryContainer = Color(0xFFFAFAFA),
tertiary = Color(0xFFFFFFFF),
onTertiary = Color(0xFF00FF00),
tertiaryContainer = Color(0xFFFFFFFF),
onTertiaryContainer = Color(0xFF00FF00),
background = Color(0xFF111111),
onBackground = Color(0xFFFFFFFF),
surface = Color(0xFF111111),
onSurface = Color(0xFFFFFFFF),
surfaceVariant = Color(0xFF212121),
onSurfaceVariant = Color(0xFFD8FFFFFF),
surfaceTint = Color(0xFF00FF00),
inverseSurface = Color(0xFFFAFAFA),
inverseOnSurface = Color(0xFF313131),
outline = Color(0xFF00FF00),
inversePrimary = Color(0xFF007700),
)
override val lightScheme = lightColorScheme(
primary = Color(0xFF00FF00),
onPrimary = Color(0xFF000000),
primaryContainer = Color(0xFF00FF00),
onPrimaryContainer = Color(0xFF000000),
secondary = Color(0xFF00FF00),
onSecondary = Color(0xFF000000),
secondaryContainer = Color(0xFF00FF00),
onSecondaryContainer = Color(0xFF000000),
tertiary = Color(0xFF000000),
onTertiary = Color(0xFF00FF00),
tertiaryContainer = Color(0xFF000000),
onTertiaryContainer = Color(0xFF00FF00),
background = Color(0xFF000000),
onBackground = Color(0xFFFFFFFF),
surface = Color(0xFF000000),
onSurface = Color(0xFFFFFFFF),
surfaceVariant = Color(0xFF111111),
onSurfaceVariant = Color(0xFFD849454E),
surfaceTint = Color(0xFF00FF00),
inverseSurface = Color(0xFF424242),
inverseOnSurface = Color(0xFFFAFAFA),
outline = Color(0xFF00FF00),
inversePrimary = Color(0xFF007700),
)
}

View file

@ -0,0 +1,71 @@
package eu.kanade.presentation.theme.colorscheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
/**
* Colors for Midnight Dusk theme
* Original color scheme by CrepeTF
* M3 color scheme generated by Material Theme Builder (https://goo.gle/material-theme-builder-web)
*
* Key colors:
* Primary #F02475
* Secondary #F02475
* Tertiary #7A5733
* Neutral #16151D
*/
internal object MidnightDuskColorScheme : BaseColorScheme() {
override val darkScheme = darkColorScheme(
primary = Color(0xFFF02475),
onPrimary = Color(0xFFFFFFFF),
primaryContainer = Color(0xFFBD1C5C),
onPrimaryContainer = Color(0xFFFFFFFF),
inversePrimary = Color(0xFFF02475),
secondary = Color(0xFFF02475),
onSecondary = Color(0xFFFFFFFF),
secondaryContainer = Color(0xFFF02475),
onSecondaryContainer = Color(0xFFFFFFFF),
tertiary = Color(0xFF55971C),
onTertiary = Color(0xFFFFFFFF),
tertiaryContainer = Color(0xFF386412),
onTertiaryContainer = Color(0xFFE5E1E5),
background = Color(0xFF16151D),
onBackground = Color(0xFFE5E1E5),
surface = Color(0xFF16151D),
onSurface = Color(0xFFE5E1E5),
surfaceVariant = Color(0xFF524346),
onSurfaceVariant = Color(0xFFD6C1C4),
surfaceTint = Color(0xFFF02475),
inverseSurface = Color(0xFF333043),
inverseOnSurface = Color(0xFFFFFFFF),
outline = Color(0xFF9F8C8F),
)
override val lightScheme = lightColorScheme(
primary = Color(0xFFBB0054),
onPrimary = Color(0xFFFFFFFF),
primaryContainer = Color(0xFFFFD9E1),
onPrimaryContainer = Color(0xFF3F0017),
inversePrimary = Color(0xFFFFB1C4),
secondary = Color(0xFFBB0054),
onSecondary = Color(0xFFFFFFFF),
secondaryContainer = Color(0xFFFFD9E1),
onSecondaryContainer = Color(0xFF3F0017),
tertiary = Color(0xFF006638),
onTertiary = Color(0xFFFFFFFF),
tertiaryContainer = Color(0xFF00894b),
onTertiaryContainer = Color(0xFF2D1600),
background = Color(0xFFFFFBFF),
onBackground = Color(0xFF1C1B1F),
surface = Color(0xFFFFFBFF),
onSurface = Color(0xFF1C1B1F),
surfaceVariant = Color(0xFFF3DDE0),
onSurfaceVariant = Color(0xFF524346),
surfaceTint = Color(0xFFBB0054),
inverseSurface = Color(0xFF313033),
inverseOnSurface = Color(0xFFF4F0F4),
outline = Color(0xFF847376),
)
}

View file

@ -0,0 +1,70 @@
package eu.kanade.presentation.theme.colorscheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
/**
* Colors for Mocha theme
* M3 colors generated by Material Theme Builder (https://goo.gle/material-theme-builder-web)
*
* Key colors:
* Primary 0xFFF38020
* Secondary 0xFFF38020
* Tertiary 0xFF1B1B22
* Neutral 0xFF655C5A
*/
internal object MochaColorScheme : BaseColorScheme() {
override val darkScheme = darkColorScheme(
primary = Color(0xFFEBC248),
onPrimary = Color(0xFF3D2F00),
primaryContainer = Color(0xFF584400),
onPrimaryContainer = Color(0xFFFFE08D),
secondary = Color(0xFFEBC248),
onSecondary = Color(0xFF3D2F00),
secondaryContainer = Color(0xFF584400),
onSecondaryContainer = Color(0xFFFFE08D),
tertiary = Color(0xFFADCFAD),
onTertiary = Color(0xFF19361F),
tertiaryContainer = Color(0xFF304D34),
onTertiaryContainer = Color(0xFFC9ECC8),
background = Color(0xFF1E1B16),
onBackground = Color(0xFFE8E1D9),
surface = Color(0xFF1E1B16),
onSurface = Color(0xFFE8E1D9),
surfaceVariant = Color(0xFF4C4639),
onSurfaceVariant = Color(0xFFCFC5B4),
surfaceTint = Color(0xFFEBC248),
inverseSurface = Color(0xFFEDE0DD),
inverseOnSurface = Color(0xFF211A18),
outline = Color(0xFF989080),
inversePrimary = Color(0xFFAE3200),
)
override val lightScheme = lightColorScheme(
primary = Color(0xFF745B00),
onPrimary = Color(0xFFFFFFFF),
primaryContainer = Color(0xFFFFE08D),
onPrimaryContainer = Color(0xFF241A00),
secondary = Color(0xFF745B00),
onSecondary = Color(0xFFFFFFFF),
secondaryContainer = Color(0xFFFFE08D),
onSecondaryContainer = Color(0xFF241A00),
tertiary = Color(0xFF47664A),
onTertiary = Color(0xFFFFFFFF),
tertiaryContainer = Color(0xFFC9ECC8),
onTertiaryContainer = Color(0xFF04210B),
background = Color(0xFFFFFBFF),
onBackground = Color(0xFF1E1B16),
surface = Color(0xFFFFFBFF),
onSurface = Color(0xFF1E1B16),
surfaceVariant = Color(0xFFEBE1CF),
onSurfaceVariant = Color(0xFF4C4639),
surfaceTint = Color(0xFF745B00),
inverseSurface = Color(0xFF362F2D),
inverseOnSurface = Color(0xFFFBEAEB),
outline = Color(0xFF7E7667),
inversePrimary = Color(0xFFFFB59D),
)
}

View file

@ -0,0 +1,125 @@
package eu.kanade.presentation.theme.colorscheme
import android.annotation.SuppressLint
import android.app.UiModeManager
import android.app.WallpaperManager
import android.content.Context
import android.graphics.Bitmap
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.ui.graphics.Color
import androidx.core.content.getSystemService
import com.google.android.material.color.utilities.Hct
import com.google.android.material.color.utilities.MaterialDynamicColors
import com.google.android.material.color.utilities.QuantizerCelebi
import com.google.android.material.color.utilities.SchemeContent
import com.google.android.material.color.utilities.Score
internal class MonetColorScheme(context: Context) : BaseColorScheme() {
private val monet = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
MonetSystemColorScheme(context)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
val seed = WallpaperManager.getInstance(context)
.getWallpaperColors(WallpaperManager.FLAG_SYSTEM)
?.primaryColor
?.toArgb()
if (seed != null) {
MonetCompatColorScheme(context, seed)
} else {
TachiyomiColorScheme
}
} else {
TachiyomiColorScheme
}
override val darkScheme
get() = monet.darkScheme
override val lightScheme
get() = monet.lightScheme
companion object {
@Suppress("Unused")
@SuppressLint("RestrictedApi")
fun extractSeedColorFromImage(bitmap: Bitmap): Int? {
val width = bitmap.width
val height = bitmap.height
val bitmapPixels = IntArray(width * height)
bitmap.getPixels(bitmapPixels, 0, width, 0, 0, width, height)
return Score.score(QuantizerCelebi.quantize(bitmapPixels, 128), 1, 0)[0]
.takeIf { it != 0 } // Don't take fallback color
}
}
}
@RequiresApi(Build.VERSION_CODES.S)
private class MonetSystemColorScheme(context: Context) : BaseColorScheme() {
override val lightScheme = dynamicLightColorScheme(context)
override val darkScheme = dynamicDarkColorScheme(context)
}
private class MonetCompatColorScheme(context: Context, seed: Int) : BaseColorScheme() {
override val lightScheme = generateColorSchemeFromSeed(context = context, seed = seed, dark = false)
override val darkScheme = generateColorSchemeFromSeed(context = context, seed = seed, dark = true)
companion object {
private fun Int.toComposeColor(): Color = Color(this)
@SuppressLint("PrivateResource", "RestrictedApi")
private fun generateColorSchemeFromSeed(context: Context, seed: Int, dark: Boolean): ColorScheme {
val scheme = SchemeContent(
Hct.fromInt(seed),
dark,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
context.getSystemService<UiModeManager>()?.contrast?.toDouble() ?: 0.0
} else {
0.0
},
)
val dynamicColors = MaterialDynamicColors()
return ColorScheme(
primary = dynamicColors.primary().getArgb(scheme).toComposeColor(),
onPrimary = dynamicColors.onPrimary().getArgb(scheme).toComposeColor(),
primaryContainer = dynamicColors.primaryContainer().getArgb(scheme).toComposeColor(),
onPrimaryContainer = dynamicColors.onPrimaryContainer().getArgb(scheme).toComposeColor(),
inversePrimary = dynamicColors.inversePrimary().getArgb(scheme).toComposeColor(),
secondary = dynamicColors.secondary().getArgb(scheme).toComposeColor(),
onSecondary = dynamicColors.onSecondary().getArgb(scheme).toComposeColor(),
secondaryContainer = dynamicColors.secondaryContainer().getArgb(scheme).toComposeColor(),
onSecondaryContainer = dynamicColors.onSecondaryContainer().getArgb(scheme).toComposeColor(),
tertiary = dynamicColors.tertiary().getArgb(scheme).toComposeColor(),
onTertiary = dynamicColors.onTertiary().getArgb(scheme).toComposeColor(),
tertiaryContainer = dynamicColors.tertiary().getArgb(scheme).toComposeColor(),
onTertiaryContainer = dynamicColors.onTertiaryContainer().getArgb(scheme).toComposeColor(),
background = dynamicColors.background().getArgb(scheme).toComposeColor(),
onBackground = dynamicColors.onBackground().getArgb(scheme).toComposeColor(),
surface = dynamicColors.surface().getArgb(scheme).toComposeColor(),
onSurface = dynamicColors.onSurface().getArgb(scheme).toComposeColor(),
surfaceVariant = dynamicColors.surfaceVariant().getArgb(scheme).toComposeColor(),
onSurfaceVariant = dynamicColors.onSurfaceVariant().getArgb(scheme).toComposeColor(),
surfaceTint = dynamicColors.surfaceTint().getArgb(scheme).toComposeColor(),
inverseSurface = dynamicColors.inverseSurface().getArgb(scheme).toComposeColor(),
inverseOnSurface = dynamicColors.inverseOnSurface().getArgb(scheme).toComposeColor(),
error = dynamicColors.error().getArgb(scheme).toComposeColor(),
onError = dynamicColors.onError().getArgb(scheme).toComposeColor(),
errorContainer = dynamicColors.errorContainer().getArgb(scheme).toComposeColor(),
onErrorContainer = dynamicColors.onErrorContainer().getArgb(scheme).toComposeColor(),
outline = dynamicColors.outline().getArgb(scheme).toComposeColor(),
outlineVariant = dynamicColors.outlineVariant().getArgb(scheme).toComposeColor(),
scrim = Color.Black,
surfaceBright = dynamicColors.surfaceBright().getArgb(scheme).toComposeColor(),
surfaceDim = dynamicColors.surfaceDim().getArgb(scheme).toComposeColor(),
surfaceContainer = dynamicColors.surfaceContainer().getArgb(scheme).toComposeColor(),
surfaceContainerHigh = dynamicColors.surfaceContainerHigh().getArgb(scheme).toComposeColor(),
surfaceContainerHighest = dynamicColors.surfaceContainerHighest().getArgb(scheme).toComposeColor(),
surfaceContainerLow = dynamicColors.surfaceContainerLow().getArgb(scheme).toComposeColor(),
surfaceContainerLowest = dynamicColors.surfaceContainerLowest().getArgb(scheme).toComposeColor(),
)
}
}
}

View file

@ -0,0 +1,71 @@
package eu.kanade.presentation.theme.colorscheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
/**
* Colors for Sapphire theme
* Original color scheme by LuftVerbot
* M3 colors generated by Material Theme Builder (https://goo.gle/material-theme-builder-web)
*
* Key colors:
* Primary 0xFFF38020
* Secondary 0xFFF38020
* Tertiary 0xFF6B5E2F
* Neutral 0xFF655C5A
*/
internal object SapphireColorScheme : BaseColorScheme() {
override val darkScheme = darkColorScheme(
primary = Color(0xFF1E88E5),
onPrimary = Color(0xFFFAFAFA),
primaryContainer = Color(0xFF1E88E5),
onPrimaryContainer = Color(0xFFFAFAFA),
inversePrimary = Color(0xFF2979FF), // Assuming 'inversePrimary' maps to 'sapphire_primaryInverse'
secondary = Color(0xFF1E88E5),
onSecondary = Color(0xFFFAFAFA),
secondaryContainer = Color(0xFF1E88E5),
onSecondaryContainer = Color(0xFFFAFAFA),
tertiary = Color(0xFF212121),
onTertiary = Color(0xFF1E88E5),
tertiaryContainer = Color(0xFF212121),
onTertiaryContainer = Color(0xFF1E88E5),
background = Color(0xFF212121),
onBackground = Color(0xFFFFFFFF),
surface = Color(0xFF212121),
onSurface = Color(0xFFFFFFFF),
surfaceVariant = Color(0xFF424242),
onSurfaceVariant = Color(0xFFD8FFFFFF),
surfaceTint = Color(0xFF1E88E5), // Assuming 'surfaceTint' maps to 'sapphire_primary' or similar
inverseSurface = Color(0xFFFAFAFA),
inverseOnSurface = Color(0xFF313131),
outline = Color(0xFF1E88E5),
)
override val lightScheme = lightColorScheme(
primary = Color(0xFF1E88E5),
onPrimary = Color(0xFFFFFFFF),
primaryContainer = Color(0xFF1E88E5),
onPrimaryContainer = Color(0xFFFFFFFF),
inversePrimary = Color(0xFF2979FF), // Assuming 'inversePrimary' maps to 'sapphire_primaryInverse'
secondary = Color(0xFF1E88E5),
onSecondary = Color(0xFFFFFFFF),
secondaryContainer = Color(0xFF1E88E5),
onSecondaryContainer = Color(0xFFFFFFFF),
tertiary = Color(0xFFE1F5FE),
onTertiary = Color(0xFF1E88E5),
tertiaryContainer = Color(0xFFE1F5FE),
onTertiaryContainer = Color(0xFF1E88E5),
background = Color(0xFFFFFFFF),
onBackground = Color(0xFF212121),
surface = Color(0xFFFFFFFF),
onSurface = Color(0xFF212121),
surfaceVariant = Color(0xFFB3E5FC),
onSurfaceVariant = Color(0xFFD849454E),
surfaceTint = Color(0xFF1E88E5), // Assuming 'surfaceTint' maps to 'sapphire_primary' or similar
inverseSurface = Color(0xFF424242),
inverseOnSurface = Color(0xFFFAFAFA),
outline = Color(0xFF1E88E5),
)
}

View file

@ -0,0 +1,71 @@
package eu.kanade.presentation.theme.colorscheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
/**
* Colors for Strawberry Daiquiri theme
* Original color scheme by Soitora
* M3 color scheme generated by Material Theme Builder (https://goo.gle/material-theme-builder-web)
*
* Key colors:
* Primary #ED4A65
* Secondary #ED4A65
* Tertiary #775930
* Neutral #655C5C
*/
internal object StrawberryColorScheme : BaseColorScheme() {
override val darkScheme = darkColorScheme(
primary = Color(0xFFFFB2B9),
onPrimary = Color(0xFF67001B),
primaryContainer = Color(0xFF91002A),
onPrimaryContainer = Color(0xFFFFDADD),
inversePrimary = Color(0xFFB61E40),
secondary = Color(0xFFFFB2B9),
onSecondary = Color(0xFF67001B),
secondaryContainer = Color(0xFF91002A),
onSecondaryContainer = Color(0xFFFFDADD),
tertiary = Color(0xFFE8C08E),
onTertiary = Color(0xFF432C06),
tertiaryContainer = Color(0xFF5D421B),
onTertiaryContainer = Color(0xFFFFDDB1),
background = Color(0xFF201A1A),
onBackground = Color(0xFFECDFDF),
surface = Color(0xFF201A1A),
onSurface = Color(0xFFECDFDF),
surfaceVariant = Color(0xFF534344),
onSurfaceVariant = Color(0xFFD7C1C2),
surfaceTint = Color(0xFFFFB2B9),
inverseSurface = Color(0xFFECDFDF),
inverseOnSurface = Color(0xFF201A1A),
outline = Color(0xFFA08C8D),
)
override val lightScheme = lightColorScheme(
primary = Color(0xFFB61E40),
onPrimary = Color(0xFFFFFFFF),
primaryContainer = Color(0xFFFFDADD),
onPrimaryContainer = Color(0xFF40000D),
inversePrimary = Color(0xFFFFB2B9),
secondary = Color(0xFFB61E40),
onSecondary = Color(0xFFFFFFFF),
secondaryContainer = Color(0xFFFFDADD),
onSecondaryContainer = Color(0xFF40000D),
tertiary = Color(0xFF775930),
onTertiary = Color(0xFFFFFFFF),
tertiaryContainer = Color(0xFFFFDDB1),
onTertiaryContainer = Color(0xFF2A1800),
background = Color(0xFFFCFCFC),
onBackground = Color(0xFF201A1A),
surface = Color(0xFFFCFCFC),
onSurface = Color(0xFF201A1A),
surfaceVariant = Color(0xFFF4DDDD),
onSurfaceVariant = Color(0xFF534344),
surfaceTint = Color(0xFFB61E40),
inverseSurface = Color(0xFF362F2F),
inverseOnSurface = Color(0xFFFBEDED),
outline = Color(0xFF857374),
)
}

View file

@ -0,0 +1,80 @@
package eu.kanade.presentation.theme.colorscheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
/**
* Colors for Default theme
* M3 colors generated by Material Theme Builder (https://goo.gle/material-theme-builder-web)
*
* Key colors:
* Primary #2979FF
* Secondary #2979FF
* Tertiary #47A84A
* Neutral #919094
*/
internal object TachiyomiColorScheme : BaseColorScheme() {
override val darkScheme = darkColorScheme(
primary = Color(0xFFB0C6FF),
onPrimary = Color(0xFF002D6E),
primaryContainer = Color(0xFF00429B),
onPrimaryContainer = Color(0xFFD9E2FF),
inversePrimary = Color(0xFF0058CA),
secondary = Color(0xFFB0C6FF),
onSecondary = Color(0xFF002D6E),
secondaryContainer = Color(0xFF00429B),
onSecondaryContainer = Color(0xFFD9E2FF),
tertiary = Color(0xFF7ADC77),
onTertiary = Color(0xFF003909),
tertiaryContainer = Color(0xFF005312),
onTertiaryContainer = Color(0xFF95F990),
background = Color(0xFF1B1B1F),
onBackground = Color(0xFFE3E2E6),
surface = Color(0xFF1B1B1F),
onSurface = Color(0xFFE3E2E6),
surfaceVariant = Color(0xFF44464F),
onSurfaceVariant = Color(0xFFC5C6D0),
surfaceTint = Color(0xFFB0C6FF),
inverseSurface = Color(0xFFE3E2E6),
inverseOnSurface = Color(0xFF1B1B1F),
error = Color(0xFFFFB4AB),
onError = Color(0xFF690005),
errorContainer = Color(0xFF93000A),
onErrorContainer = Color(0xFFFFDAD6),
outline = Color(0xFF8F9099),
outlineVariant = Color(0xFF44464F),
)
override val lightScheme = lightColorScheme(
primary = Color(0xFF0058CA),
onPrimary = Color(0xFFFFFFFF),
primaryContainer = Color(0xFFD9E2FF),
onPrimaryContainer = Color(0xFF001945),
inversePrimary = Color(0xFFB0C6FF),
secondary = Color(0xFF0058CA),
onSecondary = Color(0xFFFFFFFF),
secondaryContainer = Color(0xFFD9E2FF),
onSecondaryContainer = Color(0xFF001945),
tertiary = Color(0xFF006E1B),
onTertiary = Color(0xFFFFFFFF),
tertiaryContainer = Color(0xFF95F990),
onTertiaryContainer = Color(0xFF002203),
background = Color(0xFFFEFBFF),
onBackground = Color(0xFF1B1B1F),
surface = Color(0xFFFEFBFF),
onSurface = Color(0xFF1B1B1F),
surfaceVariant = Color(0xFFE1E2EC),
onSurfaceVariant = Color(0xFF44464F),
surfaceTint = Color(0xFF0058CA),
inverseSurface = Color(0xFF303034),
inverseOnSurface = Color(0xFFF2F0F4),
error = Color(0xFFBA1A1A),
onError = Color(0xFFFFFFFF),
errorContainer = Color(0xFFFFDAD6),
onErrorContainer = Color(0xFF410002),
outline = Color(0xFF757780),
outlineVariant = Color(0xFFC5C6D0),
)
}

View file

@ -0,0 +1,71 @@
package eu.kanade.presentation.theme.colorscheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
/**
* Colors for Tako theme
* Original color scheme by ghostbear
* M3 color scheme generated by Material Theme Builder (https://goo.gle/material-theme-builder-web)
*
* Key colors:
* Primary #F3B375
* Secondary #F3B375
* Tertiary #66577E
* Neutral #21212E
*/
internal object TakoColorScheme : BaseColorScheme() {
override val darkScheme = darkColorScheme(
primary = Color(0xFFF3B375),
onPrimary = Color(0xFF38294E),
primaryContainer = Color(0xFFF3B375),
onPrimaryContainer = Color(0xFF38294E),
inversePrimary = Color(0xFF84531E),
secondary = Color(0xFFF3B375),
onSecondary = Color(0xFF38294E),
secondaryContainer = Color(0xFFF3B375),
onSecondaryContainer = Color(0xFF38294E),
tertiary = Color(0xFF66577E),
onTertiary = Color(0xFFF3B375),
tertiaryContainer = Color(0xFF4E4065),
onTertiaryContainer = Color(0xFFEDDCFF),
background = Color(0xFF21212E),
onBackground = Color(0xFFE3E0F2),
surface = Color(0xFF21212E),
onSurface = Color(0xFFE3E0F2),
surfaceVariant = Color(0xFF49454E),
onSurfaceVariant = Color(0xFFCBC4CE),
surfaceTint = Color(0xFF66577E),
inverseSurface = Color(0xFFE5E1E6),
inverseOnSurface = Color(0xFF1B1B1E),
outline = Color(0xFF958F99),
)
override val lightScheme = lightColorScheme(
primary = Color(0xFF66577E),
onPrimary = Color(0xFFF3B375),
primaryContainer = Color(0xFF66577E),
onPrimaryContainer = Color(0xFFF3B375),
inversePrimary = Color(0xFFD6BAFF),
secondary = Color(0xFF66577E),
onSecondary = Color(0xFFF3B375),
secondaryContainer = Color(0xFF66577E),
onSecondaryContainer = Color(0xFFF3B375),
tertiary = Color(0xFFF3B375),
onTertiary = Color(0xFF574360),
tertiaryContainer = Color(0xFFFDD6B0),
onTertiaryContainer = Color(0xFF221437),
background = Color(0xFFF7F5FF),
onBackground = Color(0xFF1B1B22),
surface = Color(0xFFF7F5FF),
onSurface = Color(0xFF1B1B22),
surfaceVariant = Color(0xFFE8E0EB),
onSurfaceVariant = Color(0xFF49454E),
surfaceTint = Color(0xFF66577E),
inverseSurface = Color(0xFF313033),
inverseOnSurface = Color(0xFFF3EFF4),
outline = Color(0xFF7A757E),
)
}

View file

@ -0,0 +1,63 @@
package eu.kanade.presentation.theme.colorscheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
/**
* Colors for Teal Turqoise theme
*/
internal object TealTurqoiseColorScheme : BaseColorScheme() {
override val darkScheme = darkColorScheme(
primary = Color(0xFF40E0D0),
onPrimary = Color(0xFF000000),
primaryContainer = Color(0xFF40E0D0),
onPrimaryContainer = Color(0xFF000000),
inversePrimary = Color(0xFF008080),
secondary = Color(0xFF40E0D0),
onSecondary = Color(0xFF000000),
secondaryContainer = Color(0xFF18544E),
onSecondaryContainer = Color(0xFF40E0D0),
tertiary = Color(0xFFBF1F2F),
onTertiary = Color(0xFFFFFFFF),
tertiaryContainer = Color(0xFF200508),
onTertiaryContainer = Color(0xFFBF1F2F),
background = Color(0xFF202125),
onBackground = Color(0xFFDFDEDA),
surface = Color(0xFF202125),
onSurface = Color(0xFFDFDEDA),
surfaceVariant = Color(0xFF3F4947),
onSurfaceVariant = Color(0xFFDFDEDA),
surfaceTint = Color(0xFF40E0D0),
inverseSurface = Color(0xFFDFDEDA),
inverseOnSurface = Color(0xFF202125),
outline = Color(0xFF899391),
)
override val lightScheme = lightColorScheme(
primary = Color(0xFF008080),
onPrimary = Color(0xFFFFFFFF),
primaryContainer = Color(0xFF008080),
onPrimaryContainer = Color(0xFFFFFFFF),
inversePrimary = Color(0xFF40E0D0),
secondary = Color(0xFF008080),
onSecondary = Color(0xFFFFFFFF),
secondaryContainer = Color(0xFFBFDFDF),
onSecondaryContainer = Color(0xFF008080),
tertiary = Color(0xFFFF7F7F),
onTertiary = Color(0xFF000000),
tertiaryContainer = Color(0xFF2A1616),
onTertiaryContainer = Color(0xFFFF7F7F),
background = Color(0xFFFAFAFA),
onBackground = Color(0xFF050505),
surface = Color(0xFFFAFAFA),
onSurface = Color(0xFF050505),
surfaceVariant = Color(0xFFDAE5E2),
onSurfaceVariant = Color(0xFF050505),
surfaceTint = Color(0xFFBFDFDF),
inverseSurface = Color(0xFF050505),
inverseOnSurface = Color(0xFFFAFAFA),
outline = Color(0xFF6F7977),
)
}

View file

@ -0,0 +1,70 @@
package eu.kanade.presentation.theme.colorscheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
/**
* Colors for Tidal Wave theme
* Original color scheme by NahutabDevelop
*
* Key colors:
* Primary #004152
* Secondary #5ed4fc
* Tertiary #92f7bc
* Neutral #16151D
*/
internal object TidalWaveColorScheme : BaseColorScheme() {
override val darkScheme = darkColorScheme(
primary = Color(0xFF5ed4fc),
onPrimary = Color(0xFF003544),
primaryContainer = Color(0xFF004d61),
onPrimaryContainer = Color(0xFFb8eaff),
inversePrimary = Color(0xFFa12b03),
secondary = Color(0xFF5ed4fc),
onSecondary = Color(0xFF003544),
secondaryContainer = Color(0xFF004d61),
onSecondaryContainer = Color(0xFFb8eaff),
tertiary = Color(0xFF92f7bc),
onTertiary = Color(0xFF001c3b),
tertiaryContainer = Color(0xFFc3fada),
onTertiaryContainer = Color(0xFF78ffd6),
background = Color(0xFF001c3b),
onBackground = Color(0xFFd5e3ff),
surface = Color(0xFF001c3b),
onSurface = Color(0xFFd5e3ff),
surfaceVariant = Color(0xFF40484c),
onSurfaceVariant = Color(0xFFbfc8cc),
surfaceTint = Color(0xFF5ed4fc),
inverseSurface = Color(0xFFffe3c4),
inverseOnSurface = Color(0xFF001c3b),
outline = Color(0xFF8a9296),
)
override val lightScheme = lightColorScheme(
primary = Color(0xFF006780),
onPrimary = Color(0xFFffffff),
primaryContainer = Color(0xFFB4D4DF),
onPrimaryContainer = Color(0xFF001f28),
inversePrimary = Color(0xFFff987f),
secondary = Color(0xFF006780),
onSecondary = Color(0xFFffffff),
secondaryContainer = Color(0xFFb8eaff),
onSecondaryContainer = Color(0xFF001f28),
tertiary = Color(0xFF92f7bc),
onTertiary = Color(0xFF001c3b),
tertiaryContainer = Color(0xFFc3fada),
onTertiaryContainer = Color(0xFF78ffd6),
background = Color(0xFFfdfbff),
onBackground = Color(0xFF001c3b),
surface = Color(0xFFfdfbff),
onSurface = Color(0xFF001c3b),
surfaceVariant = Color(0xFFdce4e8),
onSurfaceVariant = Color(0xFF40484c),
surfaceTint = Color(0xFF006780),
inverseSurface = Color(0xFF020400),
inverseOnSurface = Color(0xFFffe3c4),
outline = Color(0xFF70787c),
)
}

View file

@ -0,0 +1,65 @@
package eu.kanade.presentation.theme.colorscheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
/**
* Colors for Yin & Yang theme
* Original color scheme by Riztard
* M3 colors generated by yours truly + tweaked manually
*/
internal object YinYangColorScheme : BaseColorScheme() {
override val darkScheme = darkColorScheme(
primary = Color(0xFFFFFFFF),
onPrimary = Color(0xFF5A5A5A),
primaryContainer = Color(0xFFFFFFFF),
onPrimaryContainer = Color(0xFF000000),
inversePrimary = Color(0xFFCECECE),
secondary = Color(0xFFFFFFFF),
onSecondary = Color(0xFF5A5A5A),
secondaryContainer = Color(0xFF717171),
onSecondaryContainer = Color(0xFFE4E4E4),
tertiary = Color(0xFF000000),
onTertiary = Color(0xFFFFFFFF),
tertiaryContainer = Color(0xFF00419E),
onTertiaryContainer = Color(0xFFD8E2FF),
background = Color(0xFF1E1E1E),
onBackground = Color(0xFFE6E6E6),
surface = Color(0xFF1E1E1E),
onSurface = Color(0xFFE6E6E6),
surfaceVariant = Color(0xFF4E4E4E),
onSurfaceVariant = Color(0xFFD1D1D1),
surfaceTint = Color(0xFFFFFFFF),
inverseSurface = Color(0xFFE6E6E6),
inverseOnSurface = Color(0xFF1E1E1E),
outline = Color(0xFF999999),
)
override val lightScheme = lightColorScheme(
primary = Color(0xFF000000),
onPrimary = Color(0xFFFFFFFF),
primaryContainer = Color(0xFF000000),
onPrimaryContainer = Color(0xFFFFFFFF),
inversePrimary = Color(0xFFA6A6A6),
secondary = Color(0xFF000000),
onSecondary = Color(0xFFFFFFFF),
secondaryContainer = Color(0xFFDDDDDD),
onSecondaryContainer = Color(0xFF0C0C0C),
tertiary = Color(0xFFFFFFFF),
onTertiary = Color(0xFF000000),
tertiaryContainer = Color(0xFFD8E2FF),
onTertiaryContainer = Color(0xFF001947),
background = Color(0xFFFDFDFD),
onBackground = Color(0xFF222222),
surface = Color(0xFFFDFDFD),
onSurface = Color(0xFF222222),
surfaceVariant = Color(0xFFEDEDED),
onSurfaceVariant = Color(0xFF515151),
surfaceTint = Color(0xFF000000),
inverseSurface = Color(0xFF333333),
inverseOnSurface = Color(0xFFF4F4F4),
outline = Color(0xFF838383),
)
}

View file

@ -0,0 +1,71 @@
package eu.kanade.presentation.theme.colorscheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
/**
* Colors for Yotsuba theme
* Original color scheme by ztimms73
* M3 colors generated by Material Theme Builder (https://goo.gle/material-theme-builder-web)
*
* Key colors:
* Primary 0xFFAE3200
* Secondary 0xFFAE3200
* Tertiary 0xFF6B5E2F
* Neutral 0xFF655C5A
*/
internal object YotsubaColorScheme : BaseColorScheme() {
override val darkScheme = darkColorScheme(
primary = Color(0xFFFFB59D),
onPrimary = Color(0xFF5F1600),
primaryContainer = Color(0xFF862200),
onPrimaryContainer = Color(0xFFFFDBCF),
inversePrimary = Color(0xFFAE3200),
secondary = Color(0xFFFFB59D),
onSecondary = Color(0xFF5F1600),
secondaryContainer = Color(0xFF862200),
onSecondaryContainer = Color(0xFFFFDBCF),
tertiary = Color(0xFFD7C68D),
onTertiary = Color(0xFF3A2F05),
tertiaryContainer = Color(0xFF524619),
onTertiaryContainer = Color(0xFFF5E2A7),
background = Color(0xFF211A18),
onBackground = Color(0xFFEDE0DD),
surface = Color(0xFF211A18),
onSurface = Color(0xFFEDE0DD),
surfaceVariant = Color(0xFF53433F),
onSurfaceVariant = Color(0xFFD8C2BC),
surfaceTint = Color(0xFFFFB59D),
inverseSurface = Color(0xFFEDE0DD),
inverseOnSurface = Color(0xFF211A18),
outline = Color(0xFFA08C87),
)
override val lightScheme = lightColorScheme(
primary = Color(0xFFAE3200),
onPrimary = Color(0xFFFFFFFF),
primaryContainer = Color(0xFFFFDBCF),
onPrimaryContainer = Color(0xFF3B0A00),
inversePrimary = Color(0xFFFFB59D),
secondary = Color(0xFFAE3200),
onSecondary = Color(0xFFFFFFFF),
secondaryContainer = Color(0xFFFFDBCF),
onSecondaryContainer = Color(0xFF3B0A00),
tertiary = Color(0xFF6B5E2F),
onTertiary = Color(0xFFFFFFFF),
tertiaryContainer = Color(0xFFF5E2A7),
onTertiaryContainer = Color(0xFF231B00),
background = Color(0xFFFCFCFC),
onBackground = Color(0xFF211A18),
surface = Color(0xFFFCFCFC),
onSurface = Color(0xFF211A18),
surfaceVariant = Color(0xFFF5DED8),
onSurfaceVariant = Color(0xFF53433F),
surfaceTint = Color(0xFFAE3200),
inverseSurface = Color(0xFF362F2D),
inverseOnSurface = Color(0xFFFBEEEB),
outline = Color(0xFF85736E),
)
}

View file

@ -91,7 +91,7 @@ fun AnimeUpdateScreen(
isRefreshing = false
}
},
enabled = !state.selectionMode,
enabled = { !state.selectionMode },
indicatorPadding = contentPadding,
) {
FastScrollLazyColumn(

View file

@ -87,7 +87,7 @@ fun MangaUpdateScreen(
isRefreshing = false
}
},
enabled = !state.selectionMode,
enabled = { !state.selectionMode },
indicatorPadding = contentPadding,
) {
FastScrollLazyColumn(

View file

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi
import android.annotation.SuppressLint
import android.app.ActivityManager
import android.app.Application
import android.app.PendingIntent
import android.content.BroadcastReceiver
@ -12,7 +11,6 @@ import android.os.Build
import android.os.Looper
import android.webkit.WebView
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
@ -43,6 +41,7 @@ import eu.kanade.tachiyomi.di.PreferenceModule
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.animatorDurationScale
import eu.kanade.tachiyomi.util.system.cancelNotification
@ -93,8 +92,8 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
if (packageName != process) WebView.setDataDirectorySuffix(process)
}
Injekt.importModule(AppModule(this))
Injekt.importModule(PreferenceModule(this))
Injekt.importModule(AppModule(this))
Injekt.importModule(DomainModule())
// SY -->
Injekt.importModule(SYDomainModule())
@ -173,7 +172,7 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
diskCache(diskCacheInit)
diskCache(diskCacheInit)
crossfade((300 * this@App.animatorDurationScale).toInt())
allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
allowRgb565(DeviceUtil.isLowRamDevice(this@App))
if (networkPreferences.verboseLogging().get()) logger(DebugLogger())
// Coil spawns a new thread for every image load by default

View file

@ -52,7 +52,7 @@ object Migrations {
backupPreferences: BackupPreferences,
trackerManager: TrackerManager,
): Boolean {
val lastVersionCode = preferenceStore.getInt("last_version_code", 0)
val lastVersionCode = preferenceStore.getInt(Preference.appStateKey("last_version_code"), 0)
val oldVersion = lastVersionCode.get()
if (oldVersion < BuildConfig.VERSION_CODE) {
lastVersionCode.set(BuildConfig.VERSION_CODE)
@ -368,9 +368,6 @@ object Migrations {
}
}
if (oldVersion < 84) {
if (backupPreferences.numberOfBackups().get() == 1) {
backupPreferences.numberOfBackups().set(2)
}
if (backupPreferences.backupInterval().get() == 0) {
backupPreferences.backupInterval().set(12)
BackupCreateJob.setupTask(context)
@ -516,7 +513,7 @@ object Migrations {
newKey = { Preference.privateKey(it) },
)
}
if (oldVersion < 108) {
if (oldVersion < 110) {
val prefsToReplace = listOf(
"pref_download_only",
"incognito_mode",
@ -526,6 +523,9 @@ object Migrations {
"library_update_last_timestamp",
"library_unseen_updates_count",
"last_used_category",
"last_app_check",
"last_ext_check",
"last_version_code",
)
replacePreferences(
preferenceStore = preferenceStore,

View file

@ -21,8 +21,10 @@ import eu.kanade.tachiyomi.util.system.workManager
import logcat.LogPriority
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.storage.service.StoragePreferences
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Date
import java.util.concurrent.TimeUnit
import kotlin.time.Duration.Companion.minutes
import kotlin.time.toJavaDuration
@ -38,8 +40,9 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
if (isAutoBackup && BackupRestoreJob.isRunning(context)) return Result.retry()
val backupPreferences = Injekt.get<BackupPreferences>()
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
?: backupPreferences.backupsDirectory().get().toUri()
?: getAutomaticBackupLocation()
val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupCreateFlags.AutomaticDefaults)
try {
setForeground(getForegroundInfo())
@ -49,10 +52,10 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
return try {
val location = BackupCreator(context).createBackup(uri, flags, isAutoBackup)
if (!isAutoBackup) {
notifier.showBackupComplete(
UniFile.fromUri(context, location.toUri()),
)
if (isAutoBackup) {
backupPreferences.lastAutoBackupTimestamp().set(Date().time)
} else {
notifier.showBackupComplete(UniFile.fromUri(context, location.toUri()))
}
Result.success()
} catch (e: Exception) {
@ -71,6 +74,15 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
)
}
private fun getAutomaticBackupLocation(): Uri {
val storagePreferences = Injekt.get<StoragePreferences>()
return storagePreferences.baseStorageDirectory().get().let {
val dir = UniFile.fromUri(context, it.toUri())
.createDirectory(StoragePreferences.BACKUP_DIR)
dir.uri
}
}
companion object {
fun isManualJobRunning(context: Context): Boolean {
return context.workManager.isRunning(TAG_MANUAL)

View file

@ -53,7 +53,6 @@ import tachiyomi.core.preference.Preference
import tachiyomi.core.util.system.logcat
import tachiyomi.data.handlers.anime.AnimeDatabaseHandler
import tachiyomi.data.handlers.manga.MangaDatabaseHandler
import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.category.anime.interactor.GetAnimeCategories
import tachiyomi.domain.category.manga.interactor.GetMangaCategories
import tachiyomi.domain.category.model.Category
@ -63,7 +62,6 @@ import tachiyomi.domain.entries.manga.interactor.GetMangaFavorites
import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.history.anime.interactor.GetAnimeHistory
import tachiyomi.domain.history.manga.interactor.GetMangaHistory
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.source.anime.service.AnimeSourceManager
import tachiyomi.domain.source.manga.service.MangaSourceManager
import uy.kohesive.injekt.Injekt
@ -79,8 +77,6 @@ class BackupCreator(
private val animeHandler: AnimeDatabaseHandler = Injekt.get()
private val mangaSourceManager: MangaSourceManager = Injekt.get()
private val animeSourceManager: AnimeSourceManager = Injekt.get()
private val backupPreferences: BackupPreferences = Injekt.get()
private val libraryPreferences: LibraryPreferences = Injekt.get()
private val getMangaCategories: GetMangaCategories = Injekt.get()
private val getAnimeCategories: GetAnimeCategories = Injekt.get()
private val getMangaFavorites: GetMangaFavorites = Injekt.get()
@ -125,15 +121,14 @@ class BackupCreator(
file = (
if (isAutoBackup) {
// Get dir of file and create
var dir = UniFile.fromUri(context, uri)
dir = dir.createDirectory("automatic")
val dir = UniFile.fromUri(context, uri)
.createDirectory("automatic")
// Delete older backups
val numberOfBackups = backupPreferences.numberOfBackups().get()
dir.listFiles { _, filename -> Backup.filenameRegex.matches(filename) }
.orEmpty()
.sortedByDescending { it.name }
.drop(numberOfBackups - 1)
.drop(MAX_AUTO_BACKUPS - 1)
.forEach { it.delete() }
// Create new file to place backup
@ -456,3 +451,5 @@ class BackupCreator(
return backupPreferences.filter { !Preference.isPrivate(it.key) && !Preference.isAppState(it.key) }
}
}
private val MAX_AUTO_BACKUPS: Int = 4

View file

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.data.download.anime
import android.content.Context
import androidx.core.net.toUri
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.animesource.AnimeSource
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
@ -17,6 +16,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
@ -33,10 +33,10 @@ import logcat.LogPriority
import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.lang.launchNonCancellable
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.domain.items.episode.model.Episode
import tachiyomi.domain.source.anime.service.AnimeSourceManager
import tachiyomi.domain.storage.service.StoragePreferences
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
@ -55,7 +55,7 @@ class AnimeDownloadCache(
private val provider: AnimeDownloadProvider = Injekt.get(),
private val sourceManager: AnimeSourceManager = Injekt.get(),
private val extensionManager: AnimeExtensionManager = Injekt.get(),
private val downloadPreferences: DownloadPreferences = Injekt.get(),
storagePreferences: StoragePreferences = Injekt.get(),
) {
private val scope = CoroutineScope(Dispatchers.IO)
@ -86,16 +86,9 @@ class AnimeDownloadCache(
get() = File(context.cacheDir, "dl_index_cache")
private val rootDownloadsDirLock = Mutex()
private var rootDownloadsDir = RootDirectory(getDirectoryFromPreference())
private var rootDownloadsDir = RootDirectory(provider.downloadsDir)
init {
downloadPreferences.downloadsDirectory().changes()
.onEach {
rootDownloadsDir = RootDirectory(getDirectoryFromPreference())
invalidateCache()
}
.launchIn(scope)
// Attempt to read cache file
scope.launch {
rootDownloadsDirLock.withLock {
@ -110,6 +103,14 @@ class AnimeDownloadCache(
}
}
}
storagePreferences.baseStorageDirectory().changes()
.drop(1)
.onEach {
rootDownloadsDir = RootDirectory(provider.downloadsDir)
invalidateCache()
}
.launchIn(scope)
}
/**
@ -185,7 +186,7 @@ class AnimeDownloadCache(
renewCache()
return rootDownloadsDir.sourceDirs.values.sumOf { sourceDir ->
sourceDir.dir.size()
sourceDir.dir?.size() ?: 0L
}
}
@ -300,14 +301,6 @@ class AnimeDownloadCache(
renewalJob?.cancel()
}
/**
* Returns the downloads directory from the user's preferences.
*/
private fun getDirectoryFromPreference(): UniFile {
val dir = downloadPreferences.downloadsDirectory().get()
return UniFile.fromUri(context, dir.toUri())
}
/**
* Renews the downloads cache.
*/
@ -340,7 +333,7 @@ class AnimeDownloadCache(
provider.getSourceDirName(it).lowercase() to it.id
}
val sourceDirs = rootDownloadsDir.dir.listFiles().orEmpty()
val sourceDirs = rootDownloadsDir.dir?.listFiles().orEmpty()
.filter { it.isDirectory && !it.name.isNullOrBlank() }
.mapNotNull { dir ->
val sourceId = sourceMap[dir.name!!.lowercase()]
@ -354,14 +347,14 @@ class AnimeDownloadCache(
sourceDirs.values
.map { sourceDir ->
async {
val animeDirs = sourceDir.dir.listFiles().orEmpty()
val animeDirs = sourceDir.dir?.listFiles().orEmpty()
.filter { it.isDirectory && !it.name.isNullOrBlank() }
.associate { it.name!! to AnimeDirectory(it) }
sourceDir.animeDirs = ConcurrentHashMap(animeDirs)
animeDirs.values.forEach { animeDir ->
val episodeDirs = animeDir.dir.listFiles().orEmpty()
val episodeDirs = animeDir.dir?.listFiles().orEmpty()
.mapNotNull {
when {
// Ignore incomplete downloads
@ -419,7 +412,7 @@ class AnimeDownloadCache(
* Class to store the files under the root downloads directory.
*/
private class RootDirectory(
val dir: UniFile,
val dir: UniFile?,
var sourceDirs: ConcurrentHashMap<Long, SourceDirectory> = ConcurrentHashMap(),
)
@ -427,7 +420,7 @@ private class RootDirectory(
* Class to store the files under a source directory.
*/
private class SourceDirectory(
val dir: UniFile,
val dir: UniFile?,
var animeDirs: ConcurrentHashMap<String, AnimeDirectory> = ConcurrentHashMap(),
)
@ -435,6 +428,6 @@ private class SourceDirectory(
* Class to store the files under a manga directory.
*/
private class AnimeDirectory(
val dir: UniFile,
val dir: UniFile?,
var episodeDirs: MutableSet<String> = mutableSetOf(),
)

View file

@ -18,6 +18,7 @@ import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.runBlocking
import logcat.LogPriority
import rx.Observable
import tachiyomi.core.provider.FolderProvider
import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.category.anime.interactor.GetAnimeCategories
@ -38,6 +39,7 @@ import uy.kohesive.injekt.api.get
*/
class AnimeDownloadManager(
private val context: Context,
private val folderProvider: FolderProvider,
private val provider: AnimeDownloadProvider = Injekt.get(),
private val cache: AnimeDownloadCache = Injekt.get(),
private val getCategories: GetAnimeCategories = Injekt.get(),
@ -225,7 +227,7 @@ class AnimeDownloadManager(
*/
fun getDownloadCount(anime: Anime): Int {
return if (anime.source == LocalAnimeSource.ID) {
LocalAnimeSourceFileSystem(context).getFilesInAnimeDirectory(anime.url)
LocalAnimeSourceFileSystem(folderProvider).getFilesInAnimeDirectory(anime.url)
.filter { ArchiveAnime.isSupported(it) }
.count()
} else {
@ -247,7 +249,7 @@ class AnimeDownloadManager(
*/
fun getDownloadSize(anime: Anime): Long {
return if (anime.source == LocalAnimeSource.ID) {
LocalAnimeSourceFileSystem(context).getAnimeDirectory(anime.url)
LocalAnimeSourceFileSystem(folderProvider).getAnimeDirectory(anime.url)
.let { UniFile.fromFile(it) }?.size() ?: 0L
} else {
cache.getDownloadSize(anime)

View file

@ -6,14 +6,15 @@ import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.animesource.AnimeSource
import eu.kanade.tachiyomi.util.storage.DiskUtil
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import logcat.LogPriority
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.domain.items.episode.model.Episode
import tachiyomi.domain.storage.service.StoragePreferences
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -25,26 +26,28 @@ import uy.kohesive.injekt.api.get
*/
class AnimeDownloadProvider(
private val context: Context,
downloadPreferences: DownloadPreferences = Injekt.get(),
storagePreferences: StoragePreferences = Injekt.get(),
) {
private val scope = MainScope()
private val scope = CoroutineScope(Dispatchers.IO)
/**
* The root directory for downloads.
*/
private var downloadsDir = downloadPreferences.downloadsDirectory().get().let {
val dir = UniFile.fromUri(context, it.toUri())
DiskUtil.createNoMediaFile(dir, context)
dir
}
private var _downloadsDir: UniFile? =
storagePreferences.baseStorageDirectory().get().let(::getDownloadsLocation)
val downloadsDir: UniFile?
get() = _downloadsDir
init {
downloadPreferences.downloadsDirectory().changes()
.onEach { downloadsDir = UniFile.fromUri(context, it.toUri()) }
storagePreferences.baseStorageDirectory().changes()
.onEach { _downloadsDir = getDownloadsLocation(it) }
.launchIn(scope)
}
private fun getDownloadsLocation(dir: String): UniFile? {
return UniFile.fromUri(context, dir.toUri())
?.createDirectory(StoragePreferences.DOWNLOADS_DIR)
?.also { DiskUtil.createNoMediaFile(it, context) }
}
/**
* Returns the download directory for an anime. For internal use only.
*
@ -53,7 +56,7 @@ class AnimeDownloadProvider(
*/
internal fun getAnimeDir(animeTitle: String, source: AnimeSource): UniFile {
try {
return downloadsDir
return downloadsDir!!
.createDirectory(getSourceDirName(source))
.createDirectory(getAnimeDirName(animeTitle))
} catch (e: Throwable) {
@ -68,7 +71,7 @@ class AnimeDownloadProvider(
* @param source the source to query.
*/
fun findSourceDir(source: AnimeSource): UniFile? {
return downloadsDir.findFile(getSourceDirName(source), true)
return downloadsDir?.findFile(getSourceDirName(source), true)
}
/**

View file

@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.data.download.manga
import android.app.Application
import android.content.Context
import android.net.Uri
import androidx.core.net.toUri
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import eu.kanade.tachiyomi.source.MangaSource
@ -20,6 +19,7 @@ import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
@ -44,10 +44,10 @@ import logcat.LogPriority
import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.lang.launchNonCancellable
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.items.chapter.model.Chapter
import tachiyomi.domain.source.manga.service.MangaSourceManager
import tachiyomi.domain.storage.service.StoragePreferences
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
@ -66,7 +66,7 @@ class MangaDownloadCache(
private val provider: MangaDownloadProvider = Injekt.get(),
private val sourceManager: MangaSourceManager = Injekt.get(),
private val extensionManager: MangaExtensionManager = Injekt.get(),
private val downloadPreferences: DownloadPreferences = Injekt.get(),
storagePreferences: StoragePreferences = Injekt.get(),
) {
private val scope = CoroutineScope(Dispatchers.IO)
@ -97,16 +97,9 @@ class MangaDownloadCache(
get() = File(context.cacheDir, "dl_index_cache")
private val rootDownloadsDirLock = Mutex()
private var rootDownloadsDir = RootDirectory(getDirectoryFromPreference())
private var rootDownloadsDir = RootDirectory(provider.downloadsDir)
init {
downloadPreferences.downloadsDirectory().changes()
.onEach {
rootDownloadsDir = RootDirectory(getDirectoryFromPreference())
invalidateCache()
}
.launchIn(scope)
// Attempt to read cache file
scope.launch {
rootDownloadsDirLock.withLock {
@ -121,6 +114,14 @@ class MangaDownloadCache(
}
}
}
storagePreferences.baseStorageDirectory().changes()
.drop(1)
.onEach {
rootDownloadsDir = RootDirectory(provider.downloadsDir)
invalidateCache()
}
.launchIn(scope)
}
/**
@ -200,7 +201,7 @@ class MangaDownloadCache(
renewCache()
return rootDownloadsDir.sourceDirs.values.sumOf { sourceDir ->
sourceDir.dir.size()
sourceDir.dir?.size() ?: 0L
}
}
@ -324,14 +325,6 @@ class MangaDownloadCache(
renewalJob?.cancel()
}
/**
* Returns the downloads directory from the user's preferences.
*/
private fun getDirectoryFromPreference(): UniFile {
val dir = downloadPreferences.downloadsDirectory().get()
return UniFile.fromUri(context, dir.toUri())
}
/**
* Renews the downloads cache.
*/
@ -363,7 +356,7 @@ class MangaDownloadCache(
val sourceMap = sources.associate { provider.getSourceDirName(it).lowercase() to it.id }
rootDownloadsDirLock.withLock {
val sourceDirs = rootDownloadsDir.dir.listFiles().orEmpty()
val sourceDirs = rootDownloadsDir.dir?.listFiles().orEmpty()
.filter { it.isDirectory && !it.name.isNullOrBlank() }
.mapNotNull { dir ->
val sourceId = sourceMap[dir.name!!.lowercase()]
@ -376,14 +369,14 @@ class MangaDownloadCache(
sourceDirs.values
.map { sourceDir ->
async {
val mangaDirs = sourceDir.dir.listFiles().orEmpty()
val mangaDirs = sourceDir.dir?.listFiles().orEmpty()
.filter { it.isDirectory && !it.name.isNullOrBlank() }
.associate { it.name!! to MangaDirectory(it) }
sourceDir.mangaDirs = ConcurrentHashMap(mangaDirs)
mangaDirs.values.forEach { mangaDir ->
val chapterDirs = mangaDir.dir.listFiles().orEmpty()
val chapterDirs = mangaDir.dir?.listFiles().orEmpty()
.mapNotNull {
when {
// Ignore incomplete downloads
@ -462,7 +455,7 @@ class MangaDownloadCache(
@Serializable
private class RootDirectory(
@Serializable(with = UniFileAsStringSerializer::class)
val dir: UniFile,
val dir: UniFile?,
var sourceDirs: Map<Long, SourceDirectory> = mapOf(),
)
@ -472,7 +465,7 @@ private class RootDirectory(
@Serializable
private class SourceDirectory(
@Serializable(with = UniFileAsStringSerializer::class)
val dir: UniFile,
val dir: UniFile?,
var mangaDirs: Map<String, MangaDirectory> = mapOf(),
)
@ -482,19 +475,27 @@ private class SourceDirectory(
@Serializable
private class MangaDirectory(
@Serializable(with = UniFileAsStringSerializer::class)
val dir: UniFile,
val dir: UniFile?,
var chapterDirs: MutableSet<String> = mutableSetOf(),
)
private object UniFileAsStringSerializer : KSerializer<UniFile> {
private object UniFileAsStringSerializer : KSerializer<UniFile?> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("UniFile", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: UniFile) {
return encoder.encodeString(value.uri.toString())
override fun serialize(encoder: Encoder, value: UniFile?) {
return if (value == null) {
encoder.encodeNull()
} else {
encoder.encodeString(value.uri.toString())
}
}
override fun deserialize(decoder: Decoder): UniFile {
return UniFile.fromUri(Injekt.get<Application>(), Uri.parse(decoder.decodeString()))
override fun deserialize(decoder: Decoder): UniFile? {
return if (decoder.decodeNotNullMark()) {
UniFile.fromUri(Injekt.get<Application>(), Uri.parse(decoder.decodeString()))
} else {
decoder.decodeNull()
}
}
}

View file

@ -17,6 +17,7 @@ import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.runBlocking
import logcat.LogPriority
import tachiyomi.core.provider.FolderProvider
import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.category.manga.interactor.GetMangaCategories
@ -37,6 +38,7 @@ import uy.kohesive.injekt.api.get
*/
class MangaDownloadManager(
private val context: Context,
private val folderProvider: FolderProvider,
private val provider: MangaDownloadProvider = Injekt.get(),
private val cache: MangaDownloadCache = Injekt.get(),
private val getCategories: GetMangaCategories = Injekt.get(),
@ -215,7 +217,7 @@ class MangaDownloadManager(
*/
fun getDownloadCount(manga: Manga): Int {
return if (manga.source == LocalMangaSource.ID) {
LocalMangaSourceFileSystem(context).getFilesInMangaDirectory(manga.url)
LocalMangaSourceFileSystem(folderProvider).getFilesInMangaDirectory(manga.url)
.filter { it.isDirectory || ArchiveManga.isSupported(it) }
.count()
} else {
@ -237,7 +239,7 @@ class MangaDownloadManager(
*/
fun getDownloadSize(manga: Manga): Long {
return if (manga.source == LocalMangaSource.ID) {
LocalMangaSourceFileSystem(context).getMangaDirectory(manga.url)
LocalMangaSourceFileSystem(folderProvider).getMangaDirectory(manga.url)
.let { UniFile.fromFile(it) }?.size() ?: 0L
} else {
cache.getDownloadSize(manga)

View file

@ -6,14 +6,15 @@ import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.MangaSource
import eu.kanade.tachiyomi.util.storage.DiskUtil
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import logcat.LogPriority
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.items.chapter.model.Chapter
import tachiyomi.domain.storage.service.StoragePreferences
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -25,26 +26,28 @@ import uy.kohesive.injekt.api.get
*/
class MangaDownloadProvider(
private val context: Context,
downloadPreferences: DownloadPreferences = Injekt.get(),
storagePreferences: StoragePreferences = Injekt.get(),
) {
private val scope = MainScope()
private val scope = CoroutineScope(Dispatchers.IO)
/**
* The root directory for downloads.
*/
private var downloadsDir = downloadPreferences.downloadsDirectory().get().let {
val dir = UniFile.fromUri(context, it.toUri())
DiskUtil.createNoMediaFile(dir, context)
dir
}
private var _downloadsDir: UniFile? =
storagePreferences.baseStorageDirectory().get().let(::getDownloadsLocation)
val downloadsDir: UniFile?
get() = _downloadsDir
init {
downloadPreferences.downloadsDirectory().changes()
.onEach { downloadsDir = UniFile.fromUri(context, it.toUri()) }
storagePreferences.baseStorageDirectory().changes()
.onEach { _downloadsDir = getDownloadsLocation(it) }
.launchIn(scope)
}
private fun getDownloadsLocation(dir: String): UniFile? {
return UniFile.fromUri(context, dir.toUri())
?.createDirectory(StoragePreferences.DOWNLOADS_DIR)
?.also { DiskUtil.createNoMediaFile(it, context) }
}
/**
* Returns the download directory for a manga. For internal use only.
*
@ -53,7 +56,7 @@ class MangaDownloadProvider(
*/
internal fun getMangaDir(mangaTitle: String, source: MangaSource): UniFile {
try {
return downloadsDir
return downloadsDir!!
.createDirectory(getSourceDirName(source))
.createDirectory(getMangaDirName(mangaTitle))
} catch (e: Throwable) {
@ -68,7 +71,7 @@ class MangaDownloadProvider(
* @param source the source to query.
*/
fun findSourceDir(source: MangaSource): UniFile? {
return downloadsDir.findFile(getSourceDirName(source), true)
return downloadsDir?.findFile(getSourceDirName(source), true)
}
/**

View file

@ -30,12 +30,14 @@ import tachiyomi.core.util.lang.launchUI
import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.domain.items.episode.model.Episode
import uy.kohesive.injekt.injectLazy
import java.math.RoundingMode
import java.text.NumberFormat
class AnimeLibraryUpdateNotifier(private val context: Context) {
private val preferences: SecurityPreferences by injectLazy()
private val percentFormatter = NumberFormat.getPercentInstance().apply {
roundingMode = RoundingMode.DOWN
maximumFractionDigits = 0
}
@ -79,20 +81,17 @@ class AnimeLibraryUpdateNotifier(private val context: Context) {
* @param total the total progress.
*/
fun showProgressNotification(anime: List<Anime>, current: Int, total: Int) {
if (preferences.hideNotificationContent().get()) {
progressNotificationBuilder
.setContentTitle(context.getString(R.string.notification_check_updates))
.setContentText("($current/$total)")
} else {
progressNotificationBuilder
.setContentTitle(
context.getString(
R.string.notification_updating_progress,
percentFormatter.format(current.toFloat() / total),
),
)
if (!preferences.hideNotificationContent().get()) {
val updatingText = anime.joinToString("\n") { it.title.chop(40) }
progressNotificationBuilder
.setContentTitle(
context.getString(
R.string.notification_updating_progress,
percentFormatter.format(current.toFloat() / total),
),
)
.setStyle(NotificationCompat.BigTextStyle().bigText(updatingText))
progressNotificationBuilder.setStyle(NotificationCompat.BigTextStyle().bigText(updatingText))
}
context.notify(

View file

@ -30,12 +30,14 @@ import tachiyomi.core.util.lang.launchUI
import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.items.chapter.model.Chapter
import uy.kohesive.injekt.injectLazy
import java.math.RoundingMode
import java.text.NumberFormat
class MangaLibraryUpdateNotifier(private val context: Context) {
private val preferences: SecurityPreferences by injectLazy()
private val percentFormatter = NumberFormat.getPercentInstance().apply {
roundingMode = RoundingMode.DOWN
maximumFractionDigits = 0
}
@ -79,20 +81,17 @@ class MangaLibraryUpdateNotifier(private val context: Context) {
* @param total the total progress.
*/
fun showProgressNotification(manga: List<Manga>, current: Int, total: Int) {
if (preferences.hideNotificationContent().get()) {
progressNotificationBuilder
.setContentTitle(context.getString(R.string.notification_check_updates))
.setContentText("($current/$total)")
} else {
progressNotificationBuilder
.setContentTitle(
context.getString(
R.string.notification_updating_progress,
percentFormatter.format(current.toFloat() / total),
),
)
if (!preferences.hideNotificationContent().get()) {
val updatingText = manga.joinToString("\n") { it.title.chop(40) }
progressNotificationBuilder
.setContentTitle(
context.getString(
R.string.notification_updating_progress,
percentFormatter.format(current.toFloat() / total),
),
)
.setStyle(NotificationCompat.BigTextStyle().bigText(updatingText))
progressNotificationBuilder.setStyle(NotificationCompat.BigTextStyle().bigText(updatingText))
}
context.notify(

View file

@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch
import eu.kanade.tachiyomi.data.track.model.MangaTrackSearch
import eu.kanade.tachiyomi.util.lang.htmlDecode
import kotlinx.serialization.Serializable
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
@ -27,7 +28,7 @@ data class ALManga(
title = title_user_pref
total_chapters = this@ALManga.total_chapters
cover_url = image_url_lge
summary = description ?: ""
summary = description?.htmlDecode() ?: ""
tracking_url = AnilistApi.mangaUrl(media_id)
publishing_status = this@ALManga.publishing_status
publishing_type = format
@ -58,7 +59,7 @@ data class ALAnime(
title = title_user_pref
total_episodes = this@ALAnime.total_episodes
cover_url = image_url_lge
summary = description ?: ""
summary = description?.htmlDecode() ?: ""
tracking_url = AnilistApi.animeUrl(media_id)
publishing_status = this@ALAnime.publishing_status
publishing_type = format

View file

@ -37,6 +37,7 @@ import kotlinx.serialization.json.Json
import nl.adaptivity.xmlutil.XmlDeclMode
import nl.adaptivity.xmlutil.core.XmlVersion
import nl.adaptivity.xmlutil.serialization.XML
import tachiyomi.core.provider.AndroidStorageFolderProvider
import tachiyomi.data.Database
import tachiyomi.data.DateColumnAdapter
import tachiyomi.data.StringListColumnAdapter
@ -187,11 +188,11 @@ class AppModule(val app: Application) : InjektModule {
addSingletonFactory { AnimeExtensionManager(app) }
addSingletonFactory { MangaDownloadProvider(app) }
addSingletonFactory { MangaDownloadManager(app) }
addSingletonFactory { MangaDownloadManager(app, get<AndroidStorageFolderProvider>()) }
addSingletonFactory { MangaDownloadCache(app) }
addSingletonFactory { AnimeDownloadProvider(app) }
addSingletonFactory { AnimeDownloadManager(app) }
addSingletonFactory { AnimeDownloadManager(app, get<AndroidStorageFolderProvider>()) }
addSingletonFactory { AnimeDownloadCache(app) }
addSingletonFactory { TrackerManager(app) }
@ -200,10 +201,12 @@ class AppModule(val app: Application) : InjektModule {
addSingletonFactory { ImageSaver(app) }
addSingletonFactory { LocalMangaSourceFileSystem(app) }
addSingletonFactory { AndroidStorageFolderProvider(app) }
addSingletonFactory { LocalMangaSourceFileSystem(get<AndroidStorageFolderProvider>()) }
addSingletonFactory { LocalMangaCoverManager(app, get()) }
addSingletonFactory { LocalAnimeSourceFileSystem(app) }
addSingletonFactory { LocalAnimeSourceFileSystem(get<AndroidStorageFolderProvider>()) }
addSingletonFactory { LocalAnimeCoverManager(app, get()) }
addSingletonFactory { ExternalIntents() }

View file

@ -12,20 +12,20 @@ import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.util.system.isDevFlavor
import tachiyomi.core.preference.PreferenceStore
import tachiyomi.core.provider.AndroidBackupFolderProvider
import tachiyomi.core.provider.AndroidDownloadFolderProvider
import tachiyomi.core.provider.AndroidStorageFolderProvider
import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.storage.service.StoragePreferences
import uy.kohesive.injekt.api.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar
import uy.kohesive.injekt.api.addSingletonFactory
import uy.kohesive.injekt.api.get
class PreferenceModule(val application: Application) : InjektModule {
class PreferenceModule(val app: Application) : InjektModule {
override fun InjektRegistrar.registerInjectables() {
addSingletonFactory<PreferenceStore> {
AndroidPreferenceStore(application)
AndroidPreferenceStore(app)
}
addSingletonFactory {
NetworkPreferences(
@ -52,20 +52,14 @@ class PreferenceModule(val application: Application) : InjektModule {
TrackPreferences(get())
}
addSingletonFactory {
AndroidDownloadFolderProvider(application)
DownloadPreferences(get())
}
addSingletonFactory {
DownloadPreferences(
folderProvider = get<AndroidDownloadFolderProvider>(),
preferenceStore = get(),
)
BackupPreferences(get())
}
addSingletonFactory {
AndroidBackupFolderProvider(application)
}
addSingletonFactory {
BackupPreferences(
folderProvider = get<AndroidBackupFolderProvider>(),
StoragePreferences(
folderProvider = get<AndroidStorageFolderProvider>(),
preferenceStore = get(),
)
}
@ -73,7 +67,7 @@ class PreferenceModule(val application: Application) : InjektModule {
UiPreferences(get())
}
addSingletonFactory {
BasePreferences(application, get())
BasePreferences(app, get())
}
}
}

View file

@ -29,7 +29,7 @@ internal class AnimeExtensionGithubApi {
private val json: Json by injectLazy()
private val lastExtCheck: Preference<Long> by lazy {
preferenceStore.getLong("last_ext_check", 0)
preferenceStore.getLong(Preference.appStateKey("last_ext_check"), 0)
}
private var requiresFallbackSource = false

View file

@ -6,6 +6,7 @@ import androidx.compose.material3.SnackbarResult
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.util.fastAny
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.core.preference.asState
@ -1084,6 +1085,10 @@ class MangaScreenModel(
chapters.applyFilters(manga).toList()
}
val isAnySelected by lazy {
chapters.fastAny { it.selected }
}
val chapterListItems by lazy {
processedChapters.insertSeparators { before, after ->
val (lowerChapter, higherChapter) = if (manga.sortDescending()) {

View file

@ -100,6 +100,16 @@ class ReaderPreferences(
false,
)
fun dualPageRotateToFitWebtoon() = preferenceStore.getBoolean(
"pref_dual_page_rotate_webtoon",
false,
)
fun dualPageRotateToFitInvertWebtoon() = preferenceStore.getBoolean(
"pref_dual_page_rotate_invert_webtoon",
false,
)
// endregion
// region Color filter

View file

@ -62,6 +62,18 @@ class WebtoonConfig(
readerPreferences.dualPageInvertWebtoon()
.register({ dualPageInvert = it }, { imagePropertyChangedListener?.invoke() })
readerPreferences.dualPageRotateToFitWebtoon()
.register(
{ dualPageRotateToFit = it },
{ imagePropertyChangedListener?.invoke() },
)
readerPreferences.dualPageRotateToFitInvertWebtoon()
.register(
{ dualPageRotateToFitInvert = it },
{ imagePropertyChangedListener?.invoke() },
)
readerPreferences.webtoonDoubleTapZoomEnabled()
.register(
{ doubleTapZoom = it },

View file

@ -210,6 +210,10 @@ class WebtoonPageHolder(
}
private fun process(imageStream: BufferedInputStream): InputStream {
if (viewer.config.dualPageRotateToFit) {
return rotateDualPage(imageStream)
}
if (viewer.config.dualPageSplit) {
val isDoublePage = ImageUtil.isWideImage(imageStream)
if (isDoublePage) {
@ -221,6 +225,16 @@ class WebtoonPageHolder(
return imageStream
}
private fun rotateDualPage(imageStream: BufferedInputStream): InputStream {
val isDoublePage = ImageUtil.isWideImage(imageStream)
return if (isDoublePage) {
val rotation = if (viewer.config.dualPageRotateToFitInvert) -90f else 90f
ImageUtil.rotateImage(imageStream, rotation)
} else {
imageStream
}
}
/**
* Called when the page has an error.
*/

View file

@ -3,9 +3,7 @@ package eu.kanade.tachiyomi.util.storage
import android.content.Context
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Environment
import android.os.StatFs
import androidx.core.content.ContextCompat
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.util.lang.Hash
import java.io.File
@ -64,23 +62,6 @@ object DiskUtil {
}
}
/**
* Returns the root folders of all the available external storages.
*/
fun getExternalStorages(context: Context): List<File> {
return ContextCompat.getExternalFilesDirs(context, null)
.filterNotNull()
.mapNotNull {
val file = File(it.absolutePath.substringBefore("/Android/"))
val state = Environment.getExternalStorageState(file)
if (state == Environment.MEDIA_MOUNTED || state == Environment.MEDIA_MOUNTED_READ_ONLY) {
file
} else {
null
}
}
}
/**
* Don't display downloaded chapters in gallery apps creating `.nomedia`.
*/

View file

@ -1,13 +1,16 @@
package eu.kanade.tachiyomi.util.system
import android.annotation.SuppressLint
import android.app.ActivityManager
import android.content.Context
import android.os.Build
import androidx.core.content.getSystemService
import logcat.LogPriority
import tachiyomi.core.util.system.logcat
object DeviceUtil {
val isMiui by lazy {
val isMiui: Boolean by lazy {
getSystemProperty("ro.miui.ui.version.name")?.isNotEmpty() ?: false
}
@ -16,7 +19,7 @@ object DeviceUtil {
*
* @return MIUI major version code (e.g., 13) or null if can't be parsed.
*/
val miuiMajorVersion by lazy {
val miuiMajorVersion: Int? by lazy {
if (!isMiui) return@lazy null
Build.VERSION.INCREMENTAL
@ -41,11 +44,11 @@ object DeviceUtil {
}
}
val isSamsung by lazy {
val isSamsung: Boolean by lazy {
Build.MANUFACTURER.equals("samsung", ignoreCase = true)
}
val oneUiVersion by lazy {
val oneUiVersion: Double? by lazy {
try {
val semPlatformIntField = Build.VERSION::class.java.getDeclaredField("SEM_PLATFORM_INT")
val version = semPlatformIntField.getInt(null) - 90000
@ -65,6 +68,20 @@ object DeviceUtil {
"com.zui.resolver",
)
/**
* ActivityManager#isLowRamDevice is based on a system property, which isn't
* necessarily trustworthy. 1GB is supposedly the regular threshold.
*
* Instead, we consider anything with less than 3GB of RAM as low memory
* considering how heavy image processing can be.
*/
fun isLowRamDevice(context: Context): Boolean {
val memInfo = ActivityManager.MemoryInfo()
context.getSystemService<ActivityManager>()!!.getMemoryInfo(memInfo)
val totalMemBytes = memInfo.totalMem
return totalMemBytes < 3L * 1024 * 1024 * 1024
}
@SuppressLint("PrivateApi")
private fun getSystemProperty(key: String?): String? {
return try {

View file

@ -1,24 +0,0 @@
package tachiyomi.core.provider
import android.content.Context
import android.os.Environment
import androidx.core.net.toUri
import eu.kanade.tachiyomi.core.R
import java.io.File
class AndroidDownloadFolderProvider(
val context: Context,
) : FolderProvider {
override fun directory(): File {
return File(
Environment.getExternalStorageDirectory().absolutePath + File.separator +
context.getString(R.string.app_name),
"downloads",
)
}
override fun path(): String {
return directory().toUri().toString()
}
}

View file

@ -6,7 +6,7 @@ import androidx.core.net.toUri
import eu.kanade.tachiyomi.core.R
import java.io.File
class AndroidBackupFolderProvider(
class AndroidStorageFolderProvider(
private val context: Context,
) : FolderProvider {
@ -14,7 +14,7 @@ class AndroidBackupFolderProvider(
return File(
Environment.getExternalStorageDirectory().absolutePath + File.separator +
context.getString(R.string.app_name),
"backup",
"downloads",
)
}

View file

@ -1,19 +1,16 @@
package tachiyomi.domain.backup.service
import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore
import tachiyomi.core.provider.FolderProvider
class BackupPreferences(
private val folderProvider: FolderProvider,
private val preferenceStore: PreferenceStore,
) {
fun backupsDirectory() = preferenceStore.getString("backup_directory", folderProvider.path())
fun numberOfBackups() = preferenceStore.getInt("backup_slots", 2)
fun backupInterval() = preferenceStore.getInt("backup_interval", 12)
fun lastAutoBackupTimestamp() = preferenceStore.getLong(Preference.appStateKey("last_auto_backup_timestamp"), 0L)
fun backupFlags() = preferenceStore.getStringSet(
"backup_flags",
setOf(FLAG_CATEGORIES, FLAG_CHAPTERS, FLAG_HISTORY, FLAG_TRACK),

View file

@ -1,18 +1,11 @@
package tachiyomi.domain.download.service
import tachiyomi.core.preference.PreferenceStore
import tachiyomi.core.provider.FolderProvider
class DownloadPreferences(
private val folderProvider: FolderProvider,
private val preferenceStore: PreferenceStore,
) {
fun downloadsDirectory() = preferenceStore.getString(
"download_directory",
folderProvider.path(),
)
fun downloadOnlyOverWifi() = preferenceStore.getBoolean(
"pref_download_only_over_wifi_key",
true,

View file

@ -13,7 +13,7 @@ class GetApplicationRelease(
) {
private val lastChecked: Preference<Long> by lazy {
preferenceStore.getLong("last_app_check", 0)
preferenceStore.getLong(Preference.appStateKey("last_app_check"), 0)
}
suspend fun await(arguments: Arguments): Result {

View file

@ -0,0 +1,17 @@
package tachiyomi.domain.storage.service
import tachiyomi.core.preference.PreferenceStore
import tachiyomi.core.provider.FolderProvider
class StoragePreferences(
private val folderProvider: FolderProvider,
private val preferenceStore: PreferenceStore,
) {
fun baseStorageDirectory() = preferenceStore.getString("storage_dir", folderProvider.path())
companion object {
const val BACKUP_DIR = "backup"
const val DOWNLOADS_DIR = "downloads"
}
}

View file

@ -1,5 +1,5 @@
[versions]
agp_version = "8.1.3"
agp_version = "8.1.4"
lifecycle_version = "2.6.2"
paging_version = "3.2.1"

View file

@ -1,6 +1,6 @@
[versions]
compiler = "1.5.4"
compose-bom = "2023.12.00-alpha01"
compose-bom = "2023.12.00-alpha02"
accompanist = "0.33.2-alpha"
[libraries]
@ -23,7 +23,6 @@ glance = "androidx.glance:glance-appwidget:1.0.0"
accompanist-webview = { module = "com.google.accompanist:accompanist-webview", version.ref = "accompanist" }
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
accompanist-themeadapter = { module = "com.google.accompanist:accompanist-themeadapter-material3", version.ref ="accompanist" }
accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" }
lintchecks = { module = "com.slack.lint.compose:compose-lint-checks", version = "1.2.0" }

View file

@ -24,4 +24,9 @@ tasks {
preBuild {
dependsOn(copyHebrewStrings, localesConfigTask)
}
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions.freeCompilerArgs += listOf(
"-Xexpect-actual-classes",
)
}
}

View file

@ -434,9 +434,7 @@
<string name="pref_lowest">Lowest</string>
<!-- Downloads section -->
<string name="pref_download_directory">Download location</string>
<string name="pref_remove_exclude_categories">Excluded categories</string>
<string name="custom_dir">Custom location</string>
<string name="invalid_location">Invalid location: %s</string>
<string name="disabled">Disabled</string>
<string name="last_read_chapter">Last read chapter</string>
@ -473,13 +471,13 @@
<string name="pref_hide_in_library_items">Hide entries already in library</string>
<!-- Data and storage section -->
<string name="pref_storage_location">Storage location</string>
<string name="pref_storage_location_info">Used for automatic backups, chapter downloads, and local source.</string>
<string name="pref_create_backup">Create backup</string>
<string name="pref_create_backup_summ">Can be used to restore current library</string>
<string name="pref_restore_backup">Restore backup</string>
<string name="pref_restore_backup_summ">Restore library from backup file</string>
<string name="pref_backup_directory">Backup location</string>
<string name="pref_backup_interval">Automatic backup frequency</string>
<string name="pref_backup_slots">Maximum automatic backups</string>
<string name="action_create">Create</string>
<string name="backup_created">Backup created</string>
<string name="invalid_backup_file">Invalid backup file</string>
@ -827,7 +825,6 @@
<string name="download_queue_error">Couldn\'t download chapters. You can try again in the downloads section</string>
<!-- Library update service notifications -->
<string name="notification_check_updates">Checking for new chapters</string>
<string name="notification_updating_progress">Updating library… (%s)</string>
<string name="notification_size_warning">Large updates harm sources and may lead to slower updates and also increased battery usage. Tap to learn more.</string>
<string name="notification_new_chapters">New chapters found</string>

View file

@ -44,7 +44,6 @@ import androidx.compose.ui.unit.dp
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import tachiyomi.presentation.core.components.material.padding
@ -126,7 +125,6 @@ private fun <T> WheelPicker(
snapshotFlow { lazyListState.firstVisibleItemScrollOffset }
.map { calculateSnappedItemIndex(lazyListState) }
.distinctUntilChanged()
.drop(1)
.collectLatest {
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
internalOnSelectionChanged(it)

View file

@ -1,17 +1,34 @@
package tachiyomi.presentation.core.components.material
import androidx.compose.animation.core.animate
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.pulltorefresh.PullToRefreshContainer
import androidx.compose.material3.pulltorefresh.PullToRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import kotlin.math.abs
import kotlin.math.pow
/**
* @param refreshing Whether the layout is currently refreshing
@ -19,38 +36,247 @@ import androidx.compose.ui.unit.dp
* @param enabled Whether the the layout should react to swipe gestures or not.
* @param indicatorPadding Content padding for the indicator, to inset the indicator in if required.
* @param content The content containing a vertically scrollable composable.
*
* Code reference: [Accompanist SwipeRefresh](https://github.com/google/accompanist/blob/677bc4ca0ee74677a8ba73793d04d85fe4ab55fb/swiperefresh/src/main/java/com/google/accompanist/swiperefresh/SwipeRefresh.kt#L265-L283)
*/
@Composable
fun PullRefresh(
refreshing: Boolean,
enabled: () -> Boolean,
onRefresh: () -> Unit,
enabled: Boolean,
modifier: Modifier = Modifier,
indicatorPadding: PaddingValues = PaddingValues(0.dp),
content: @Composable () -> Unit,
) {
val state = rememberPullRefreshState(
refreshing = refreshing,
onRefresh = onRefresh,
val state = rememberPullToRefreshState(
initialRefreshing = refreshing,
extraVerticalOffset = indicatorPadding.calculateTopPadding(),
enabled = enabled,
)
Box(Modifier.pullRefresh(state, enabled)) {
content()
Box(
Modifier
.padding(indicatorPadding)
.matchParentSize()
.clipToBounds(),
) {
PullRefreshIndicator(
refreshing = refreshing,
state = state,
modifier = Modifier.align(Alignment.TopCenter),
backgroundColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary,
)
if (state.isRefreshing) {
LaunchedEffect(true) {
onRefresh()
}
}
LaunchedEffect(refreshing) {
if (refreshing && !state.isRefreshing) {
state.startRefreshAnimated()
} else if (!refreshing && state.isRefreshing) {
state.endRefreshAnimated()
}
}
Box(modifier.nestedScroll(state.nestedScrollConnection)) {
content()
val contentPadding = remember(indicatorPadding) {
object : PaddingValues {
override fun calculateLeftPadding(layoutDirection: LayoutDirection): Dp =
indicatorPadding.calculateLeftPadding(layoutDirection)
override fun calculateTopPadding(): Dp = 0.dp
override fun calculateRightPadding(layoutDirection: LayoutDirection): Dp =
indicatorPadding.calculateRightPadding(layoutDirection)
override fun calculateBottomPadding(): Dp =
indicatorPadding.calculateBottomPadding()
}
}
PullToRefreshContainer(
state = state,
modifier = Modifier
.align(Alignment.TopCenter)
.padding(contentPadding),
containerColor = MaterialTheme.colorScheme.surfaceVariant,
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
@Composable
private fun rememberPullToRefreshState(
initialRefreshing: Boolean,
extraVerticalOffset: Dp,
positionalThreshold: Dp = 64.dp,
enabled: () -> Boolean = { true },
): PullToRefreshStateImpl {
val density = LocalDensity.current
val extraVerticalOffsetPx = with(density) { extraVerticalOffset.toPx() }
val positionalThresholdPx = with(density) { positionalThreshold.toPx() }
return rememberSaveable(
extraVerticalOffset,
positionalThresholdPx,
enabled,
saver = PullToRefreshStateImpl.Saver(
extraVerticalOffset = extraVerticalOffsetPx,
positionalThreshold = positionalThresholdPx,
enabled = enabled,
),
) {
PullToRefreshStateImpl(
initialRefreshing = initialRefreshing,
extraVerticalOffset = extraVerticalOffsetPx,
positionalThreshold = positionalThresholdPx,
enabled = enabled,
)
}
}
/**
* Creates a [PullToRefreshState].
*
* @param positionalThreshold The positional threshold, in pixels, in which a refresh is triggered
* @param extraVerticalOffset Extra vertical offset, in pixels, for the "refreshing" state
* @param initialRefreshing The initial refreshing value of [PullToRefreshState]
* @param enabled a callback used to determine whether scroll events are to be handled by this
* [PullToRefreshState]
*/
private class PullToRefreshStateImpl(
initialRefreshing: Boolean,
private val extraVerticalOffset: Float,
override val positionalThreshold: Float,
enabled: () -> Boolean,
) : PullToRefreshState {
override val progress get() = adjustedDistancePulled / positionalThreshold
override var verticalOffset by mutableFloatStateOf(if (initialRefreshing) refreshingVerticalOffset else 0f)
override var isRefreshing by mutableStateOf(initialRefreshing)
private val refreshingVerticalOffset: Float
get() = positionalThreshold + extraVerticalOffset
override fun startRefresh() {
isRefreshing = true
verticalOffset = refreshingVerticalOffset
}
suspend fun startRefreshAnimated() {
isRefreshing = true
animateTo(refreshingVerticalOffset)
}
override fun endRefresh() {
verticalOffset = 0f
isRefreshing = false
}
suspend fun endRefreshAnimated() {
animateTo(0f)
isRefreshing = false
}
override var nestedScrollConnection = object : NestedScrollConnection {
override fun onPreScroll(
available: Offset,
source: NestedScrollSource,
): Offset = when {
!enabled() -> Offset.Zero
// Swiping up
source == NestedScrollSource.Drag && available.y < 0 -> {
consumeAvailableOffset(available)
}
else -> Offset.Zero
}
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource,
): Offset = when {
!enabled() -> Offset.Zero
// Swiping down
source == NestedScrollSource.Drag && available.y > 0 -> {
consumeAvailableOffset(available)
}
else -> Offset.Zero
}
override suspend fun onPreFling(available: Velocity): Velocity {
return Velocity(0f, onRelease(available.y))
}
}
/** Helper method for nested scroll connection */
fun consumeAvailableOffset(available: Offset): Offset {
val y = if (isRefreshing) {
0f
} else {
val newOffset = (distancePulled + available.y).coerceAtLeast(0f)
val dragConsumed = newOffset - distancePulled
distancePulled = newOffset
verticalOffset = calculateVerticalOffset() + (extraVerticalOffset * progress.coerceIn(0f, 1f))
dragConsumed
}
return Offset(0f, y)
}
/** Helper method for nested scroll connection. Calls onRefresh callback when triggered */
suspend fun onRelease(velocity: Float): Float {
if (isRefreshing) return 0f // Already refreshing, do nothing
// Trigger refresh
if (adjustedDistancePulled > positionalThreshold) {
startRefreshAnimated()
} else {
animateTo(0f)
}
val consumed = when {
// We are flinging without having dragged the pull refresh (for example a fling inside
// a list) - don't consume
distancePulled == 0f -> 0f
// If the velocity is negative, the fling is upwards, and we don't want to prevent the
// the list from scrolling
velocity < 0f -> 0f
// We are showing the indicator, and the fling is downwards - consume everything
else -> velocity
}
distancePulled = 0f
return consumed
}
suspend fun animateTo(offset: Float) {
animate(initialValue = verticalOffset, targetValue = offset) { value, _ ->
verticalOffset = value
}
}
/** Provides custom vertical offset behavior for [PullToRefreshContainer] */
fun calculateVerticalOffset(): Float = when {
// If drag hasn't gone past the threshold, the position is the adjustedDistancePulled.
adjustedDistancePulled <= positionalThreshold -> adjustedDistancePulled
else -> {
// How far beyond the threshold pull has gone, as a percentage of the threshold.
val overshootPercent = abs(progress) - 1.0f
// Limit the overshoot to 200%. Linear between 0 and 200.
val linearTension = overshootPercent.coerceIn(0f, 2f)
// Non-linear tension. Increases with linearTension, but at a decreasing rate.
val tensionPercent = linearTension - linearTension.pow(2) / 4
// The additional offset beyond the threshold.
val extraOffset = positionalThreshold * tensionPercent
positionalThreshold + extraOffset
}
}
companion object {
/** The default [Saver] for [PullToRefreshStateImpl]. */
fun Saver(
extraVerticalOffset: Float,
positionalThreshold: Float,
enabled: () -> Boolean,
) = Saver<PullToRefreshStateImpl, Boolean>(
save = { it.isRefreshing },
restore = { isRefreshing ->
PullToRefreshStateImpl(
initialRefreshing = isRefreshing,
extraVerticalOffset = extraVerticalOffset,
positionalThreshold = positionalThreshold,
enabled = enabled,
)
},
)
}
private var distancePulled by mutableFloatStateOf(0f)
private val adjustedDistancePulled: Float get() = distancePulled * 0.5f
}

View file

@ -35,3 +35,12 @@ android {
consumerProguardFile("consumer-proguard.pro")
}
}
tasks {
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions.freeCompilerArgs += listOf(
"-Xexpect-actual-classes",
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
)
}
}

View file

@ -9,7 +9,6 @@ import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.toFFmpegString
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
@ -61,11 +60,11 @@ actual class LocalAnimeSource(
override suspend fun getLatestUpdates(page: Int) = getSearchAnime(page, "", LATEST_FILTERS)
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
val baseDirsFiles = fileSystem.getFilesInBaseDirectories()
val baseDirFiles = fileSystem.getFilesInBaseDirectory()
val lastModifiedLimit by
lazy { if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L }
var animeDirs = baseDirsFiles
var animeDirs = baseDirFiles
// Filter out files that are hidden and is not a folder
.filter { it.isDirectory && !it.name.startsWith('.') }
.distinctBy { it.name }
@ -148,10 +147,10 @@ actual class LocalAnimeSource(
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchAnime"))
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
val baseDirsFiles = fileSystem.getFilesInBaseDirectories()
val baseDirFiles = fileSystem.getFilesInBaseDirectory()
val lastModifiedLimit by
lazy { if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L }
var animeDirs = baseDirsFiles
var animeDirs = baseDirFiles
// Filter out files that are hidden and is not a folder
.filter { it.isDirectory && !it.name.startsWith('.') }
.distinctBy { it.name }
@ -305,8 +304,8 @@ actual class LocalAnimeSource(
)
private fun updateCoverFromVideo(episode: SEpisode, anime: SAnime) {
val baseDirsFiles = getBaseDirectoriesFiles(context)
val animeDir = getAnimeDir(anime.url, baseDirsFiles) ?: return
val baseDirFiles = getBaseDirectoryFiles()
val animeDir = getAnimeDir(anime.url, baseDirFiles) ?: return
val coverPath = "${animeDir.absolutePath}/$DEFAULT_COVER_NAME"
val episodeFilename = { episode.url.toFFmpegString(context) }
@ -325,6 +324,25 @@ actual class LocalAnimeSource(
}
}
private fun getBaseDirectoryFiles(): List<File> {
val baseDir = fileSystem.getBaseDirectory()
fun getAllFiles(dir: File, accumulator: MutableList<File>) {
dir.listFiles()?.forEach { file ->
if (file.isDirectory) {
getAllFiles(file, accumulator)
} else {
accumulator.add(file)
}
}
}
val allFiles = mutableListOf<File>()
getAllFiles(baseDir, allFiles)
return allFiles
}
companion object {
const val ID = 0L
const val HELP_URL = "https://aniyomi.org/help/guides/local-anime/"
@ -332,20 +350,7 @@ actual class LocalAnimeSource(
private const val DEFAULT_COVER_NAME = "cover.jpg"
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
private fun getBaseDirectories(context: Context): Sequence<File> {
val localFolder = context.getString(R.string.app_name) + File.separator + "localanime"
return DiskUtil.getExternalStorages(context)
.map { File(it.absolutePath, localFolder) }
.asSequence()
}
private fun getBaseDirectoriesFiles(context: Context): Sequence<File> {
return getBaseDirectories(context)
// Get all the files inside all baseDir
.flatMap { it.listFiles().orEmpty().toList() }
}
private fun getAnimeDir(animeUrl: String, baseDirsFile: Sequence<File>): File? {
private fun getAnimeDir(animeUrl: String, baseDirsFile: List<File>): File? {
return baseDirsFile
// Get the first animeDir or null
.firstOrNull { it.isDirectory && it.name == animeUrl }

View file

@ -75,11 +75,11 @@ actual class LocalMangaSource(
override suspend fun getLatestUpdates(page: Int) = getSearchManga(page, "", LATEST_FILTERS)
override suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage {
val baseDirsFiles = fileSystem.getFilesInBaseDirectories()
val baseDirFiles = fileSystem.getFilesInBaseDirectory()
val lastModifiedLimit by
lazy { if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L }
var mangaDirs = baseDirsFiles
var mangaDirs = baseDirFiles
// Filter out files that are hidden and is not a folder
.filter { it.isDirectory && !it.name.startsWith('.') }
.distinctBy { it.name }
@ -341,9 +341,8 @@ actual class LocalMangaSource(
fun getFormat(chapter: SChapter): Format {
try {
return fileSystem.getBaseDirectories()
.map { dir -> File(dir, chapter.url) }
.find { it.exists() }
return File(fileSystem.getBaseDirectory(), chapter.url)
.takeIf { it.exists() }
?.let(Format.Companion::valueOf)
?: throw Exception(context.getString(R.string.chapter_not_found))
} catch (e: Format.UnknownFormatException) {

View file

@ -1,36 +1,28 @@
package tachiyomi.source.local.io.anime
import android.content.Context
import eu.kanade.tachiyomi.util.storage.DiskUtil
import tachiyomi.source.local.R
import tachiyomi.core.provider.FolderProvider
import java.io.File
actual class LocalAnimeSourceFileSystem(
private val context: Context,
private val folderProvider: FolderProvider,
) {
private val baseFolderLocation = "${context.getString(R.string.app_name)}${File.separator}localanime"
actual fun getBaseDirectories(): Sequence<File> {
return DiskUtil.getExternalStorages(context)
.map { File(it.absolutePath, baseFolderLocation) }
.asSequence()
actual fun getBaseDirectory(): File {
return File(folderProvider.directory(), "localanime")
}
actual fun getFilesInBaseDirectories(): Sequence<File> {
return getBaseDirectories()
// Get all the files inside all baseDir
.flatMap { it.listFiles().orEmpty().toList() }
actual fun getFilesInBaseDirectory(): List<File> {
return getBaseDirectory().listFiles().orEmpty().toList()
}
actual fun getAnimeDirectory(name: String): File? {
return getFilesInBaseDirectories()
return getFilesInBaseDirectory()
// Get the first animeDir or null
.firstOrNull { it.isDirectory && it.name == name }
}
actual fun getFilesInAnimeDirectory(name: String): Sequence<File> {
return getFilesInBaseDirectories()
actual fun getFilesInAnimeDirectory(name: String): List<File> {
return getFilesInBaseDirectory()
// Filter out ones that are not related to the anime and is not a directory
.filter { it.isDirectory && it.name == name }
// Get all the files inside the filtered folders

View file

@ -1,36 +1,27 @@
package tachiyomi.source.local.io.manga
import android.content.Context
import eu.kanade.tachiyomi.util.storage.DiskUtil
import tachiyomi.source.local.R
import tachiyomi.core.provider.FolderProvider
import java.io.File
actual class LocalMangaSourceFileSystem(
private val context: Context,
private val folderProvider: FolderProvider,
) {
private val baseFolderLocation = "${context.getString(R.string.app_name)}${File.separator}local"
actual fun getBaseDirectories(): Sequence<File> {
return DiskUtil.getExternalStorages(context)
.map { File(it.absolutePath, baseFolderLocation) }
.asSequence()
actual fun getBaseDirectory(): File {
return File(folderProvider.directory(), "local")
}
actual fun getFilesInBaseDirectories(): Sequence<File> {
return getBaseDirectories()
// Get all the files inside all baseDir
.flatMap { it.listFiles().orEmpty().toList() }
actual fun getFilesInBaseDirectory(): List<File> {
return getBaseDirectory().listFiles().orEmpty().toList()
}
actual fun getMangaDirectory(name: String): File? {
return getFilesInBaseDirectories()
return getFilesInBaseDirectory()
// Get the first mangaDir or null
.firstOrNull { it.isDirectory && it.name == name }
}
actual fun getFilesInMangaDirectory(name: String): Sequence<File> {
return getFilesInBaseDirectories()
actual fun getFilesInMangaDirectory(name: String): List<File> {
return getFilesInBaseDirectory()
// Filter out ones that are not related to the manga and is not a directory
.filter { it.isDirectory && it.name == name }
// Get all the files inside the filtered folders

View file

@ -4,11 +4,11 @@ import java.io.File
expect class LocalAnimeSourceFileSystem {
fun getBaseDirectories(): Sequence<File>
fun getBaseDirectory(): File
fun getFilesInBaseDirectories(): Sequence<File>
fun getFilesInBaseDirectory(): List<File>
fun getAnimeDirectory(name: String): File?
fun getFilesInAnimeDirectory(name: String): Sequence<File>
fun getFilesInAnimeDirectory(name: String): List<File>
}

View file

@ -4,11 +4,11 @@ import java.io.File
expect class LocalMangaSourceFileSystem {
fun getBaseDirectories(): Sequence<File>
fun getBaseDirectory(): File
fun getFilesInBaseDirectories(): Sequence<File>
fun getFilesInBaseDirectory(): List<File>
fun getMangaDirectory(name: String): File?
fun getFilesInMangaDirectory(name: String): Sequence<File>
fun getFilesInMangaDirectory(name: String): List<File>
}