mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-21 12:17:12 +03:00
parent
2b9fe5c389
commit
807ea3d1a5
79 changed files with 2248 additions and 698 deletions
|
@ -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)
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -94,7 +94,7 @@ fun AnimeLibraryContent(
|
|||
isRefreshing = false
|
||||
}
|
||||
},
|
||||
enabled = notSelectionMode,
|
||||
enabled = { notSelectionMode },
|
||||
) {
|
||||
AnimeLibraryPager(
|
||||
state = pagerState,
|
||||
|
|
|
@ -94,7 +94,7 @@ fun MangaLibraryContent(
|
|||
isRefreshing = false
|
||||
}
|
||||
},
|
||||
enabled = notSelectionMode,
|
||||
enabled = { notSelectionMode },
|
||||
) {
|
||||
MangaLibraryPager(
|
||||
state = pagerState,
|
||||
|
|
|
@ -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)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
)
|
||||
}
|
|
@ -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),
|
||||
)
|
||||
}
|
|
@ -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),
|
||||
)
|
||||
}
|
|
@ -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),
|
||||
)
|
||||
}
|
|
@ -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),
|
||||
)
|
||||
}
|
|
@ -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),
|
||||
)
|
||||
}
|
|
@ -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),
|
||||
)
|
||||
}
|
|
@ -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),
|
||||
)
|
||||
}
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
)
|
||||
}
|
|
@ -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),
|
||||
)
|
||||
}
|
|
@ -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),
|
||||
)
|
||||
}
|
|
@ -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),
|
||||
)
|
||||
}
|
|
@ -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),
|
||||
)
|
||||
}
|
|
@ -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),
|
||||
)
|
||||
}
|
|
@ -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),
|
||||
)
|
||||
}
|
|
@ -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),
|
||||
)
|
||||
}
|
|
@ -91,7 +91,7 @@ fun AnimeUpdateScreen(
|
|||
isRefreshing = false
|
||||
}
|
||||
},
|
||||
enabled = !state.selectionMode,
|
||||
enabled = { !state.selectionMode },
|
||||
indicatorPadding = contentPadding,
|
||||
) {
|
||||
FastScrollLazyColumn(
|
||||
|
|
|
@ -87,7 +87,7 @@ fun MangaUpdateScreen(
|
|||
isRefreshing = false
|
||||
}
|
||||
},
|
||||
enabled = !state.selectionMode,
|
||||
enabled = { !state.selectionMode },
|
||||
indicatorPadding = contentPadding,
|
||||
) {
|
||||
FastScrollLazyColumn(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() }
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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`.
|
||||
*/
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
)
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -24,4 +24,9 @@ tasks {
|
|||
preBuild {
|
||||
dependsOn(copyHebrewStrings, localesConfigTask)
|
||||
}
|
||||
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
|
||||
kotlinOptions.freeCompilerArgs += listOf(
|
||||
"-Xexpect-actual-classes",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue