mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-24 21:58:34 +03:00
parent
5a2a3fd080
commit
5ceae3116b
38 changed files with 374 additions and 393 deletions
|
@ -218,6 +218,7 @@ dependencies {
|
||||||
// Disk
|
// Disk
|
||||||
implementation(libs.disklrucache)
|
implementation(libs.disklrucache)
|
||||||
implementation(libs.unifile)
|
implementation(libs.unifile)
|
||||||
|
implementation(libs.compress)
|
||||||
implementation(libs.junrar)
|
implementation(libs.junrar)
|
||||||
|
|
||||||
// Preferences
|
// Preferences
|
||||||
|
|
|
@ -6,13 +6,14 @@ import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import tachiyomi.domain.source.anime.model.AnimeSource
|
import tachiyomi.domain.source.anime.model.AnimeSource
|
||||||
import tachiyomi.domain.source.anime.repository.AnimeSourceRepository
|
import tachiyomi.domain.source.anime.repository.AnimeSourceRepository
|
||||||
|
import java.util.SortedMap
|
||||||
|
|
||||||
class GetLanguagesWithAnimeSources(
|
class GetLanguagesWithAnimeSources(
|
||||||
private val repository: AnimeSourceRepository,
|
private val repository: AnimeSourceRepository,
|
||||||
private val preferences: SourcePreferences,
|
private val preferences: SourcePreferences,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun subscribe(): Flow<Map<String, List<AnimeSource>>> {
|
fun subscribe(): Flow<SortedMap<String, List<AnimeSource>>> {
|
||||||
return combine(
|
return combine(
|
||||||
preferences.enabledLanguages().changes(),
|
preferences.enabledLanguages().changes(),
|
||||||
preferences.disabledAnimeSources().changes(),
|
preferences.disabledAnimeSources().changes(),
|
||||||
|
@ -23,7 +24,8 @@ class GetLanguagesWithAnimeSources(
|
||||||
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name },
|
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name },
|
||||||
)
|
)
|
||||||
|
|
||||||
sortedSources.groupBy { it.lang }
|
sortedSources
|
||||||
|
.groupBy { it.lang }
|
||||||
.toSortedMap(
|
.toSortedMap(
|
||||||
compareBy<String> { it !in enabledLanguage }.then(LocaleHelper.comparator),
|
compareBy<String> { it !in enabledLanguage }.then(LocaleHelper.comparator),
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,13 +6,14 @@ import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import tachiyomi.domain.source.manga.model.Source
|
import tachiyomi.domain.source.manga.model.Source
|
||||||
import tachiyomi.domain.source.manga.repository.MangaSourceRepository
|
import tachiyomi.domain.source.manga.repository.MangaSourceRepository
|
||||||
|
import java.util.SortedMap
|
||||||
|
|
||||||
class GetLanguagesWithMangaSources(
|
class GetLanguagesWithMangaSources(
|
||||||
private val repository: MangaSourceRepository,
|
private val repository: MangaSourceRepository,
|
||||||
private val preferences: SourcePreferences,
|
private val preferences: SourcePreferences,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun subscribe(): Flow<Map<String, List<Source>>> {
|
fun subscribe(): Flow<SortedMap<String, List<Source>>> {
|
||||||
return combine(
|
return combine(
|
||||||
preferences.enabledLanguages().changes(),
|
preferences.enabledLanguages().changes(),
|
||||||
preferences.disabledMangaSources().changes(),
|
preferences.disabledMangaSources().changes(),
|
||||||
|
@ -23,7 +24,8 @@ class GetLanguagesWithMangaSources(
|
||||||
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name },
|
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name },
|
||||||
)
|
)
|
||||||
|
|
||||||
sortedSources.groupBy { it.lang }
|
sortedSources
|
||||||
|
.groupBy { it.lang }
|
||||||
.toSortedMap(
|
.toSortedMap(
|
||||||
compareBy<String> { it !in enabledLanguage }.then(LocaleHelper.comparator),
|
compareBy<String> { it !in enabledLanguage }.then(LocaleHelper.comparator),
|
||||||
)
|
)
|
||||||
|
|
|
@ -64,7 +64,7 @@ private fun AnimeSourcesFilterContent(
|
||||||
state.items.forEach { (language, sources) ->
|
state.items.forEach { (language, sources) ->
|
||||||
val enabled = language in state.enabledLanguages
|
val enabled = language in state.enabledLanguages
|
||||||
item(
|
item(
|
||||||
key = language.hashCode(),
|
key = language,
|
||||||
contentType = "source-filter-header",
|
contentType = "source-filter-header",
|
||||||
) {
|
) {
|
||||||
AnimeSourcesFilterHeader(
|
AnimeSourcesFilterHeader(
|
||||||
|
@ -74,18 +74,19 @@ private fun AnimeSourcesFilterContent(
|
||||||
onClickItem = onClickLanguage,
|
onClickItem = onClickLanguage,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (!enabled) return@forEach
|
if (enabled) {
|
||||||
items(
|
items(
|
||||||
items = sources,
|
items = sources,
|
||||||
key = { "source-filter-${it.key()}" },
|
key = { "source-filter-${it.key()}" },
|
||||||
contentType = { "source-filter-item" },
|
contentType = { "source-filter-item" },
|
||||||
) { source ->
|
) { source ->
|
||||||
AnimeSourcesFilterItem(
|
AnimeSourcesFilterItem(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemPlacement(),
|
||||||
source = source,
|
source = source,
|
||||||
isEnabled = "${source.id}" !in state.disabledSources,
|
isEnabled = "${source.id}" !in state.disabledSources,
|
||||||
onClickItem = onClickSource,
|
onClickItem = onClickSource,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.paging.LoadState
|
import androidx.paging.LoadState
|
||||||
import androidx.paging.compose.LazyPagingItems
|
import androidx.paging.compose.LazyPagingItems
|
||||||
|
import androidx.paging.compose.itemKey
|
||||||
import eu.kanade.presentation.browse.InLibraryBadge
|
import eu.kanade.presentation.browse.InLibraryBadge
|
||||||
import eu.kanade.presentation.browse.manga.components.BrowseSourceLoadingItem
|
import eu.kanade.presentation.browse.manga.components.BrowseSourceLoadingItem
|
||||||
import eu.kanade.presentation.library.CommonEntryItemDefaults
|
import eu.kanade.presentation.library.CommonEntryItemDefaults
|
||||||
|
@ -40,7 +41,10 @@ fun BrowseAnimeSourceComfortableGrid(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items(animeList.itemCount) { index ->
|
items(
|
||||||
|
count = animeList.itemCount,
|
||||||
|
key = animeList.itemKey { it.value.id },
|
||||||
|
) { index ->
|
||||||
val anime by animeList[index]?.collectAsState() ?: return@items
|
val anime by animeList[index]?.collectAsState() ?: return@items
|
||||||
BrowseAnimeSourceComfortableGridItem(
|
BrowseAnimeSourceComfortableGridItem(
|
||||||
anime = anime,
|
anime = anime,
|
||||||
|
|
|
@ -11,6 +11,7 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.paging.LoadState
|
import androidx.paging.LoadState
|
||||||
import androidx.paging.compose.LazyPagingItems
|
import androidx.paging.compose.LazyPagingItems
|
||||||
|
import androidx.paging.compose.itemKey
|
||||||
import eu.kanade.presentation.browse.InLibraryBadge
|
import eu.kanade.presentation.browse.InLibraryBadge
|
||||||
import eu.kanade.presentation.browse.manga.components.BrowseSourceLoadingItem
|
import eu.kanade.presentation.browse.manga.components.BrowseSourceLoadingItem
|
||||||
import eu.kanade.presentation.library.CommonEntryItemDefaults
|
import eu.kanade.presentation.library.CommonEntryItemDefaults
|
||||||
|
@ -40,7 +41,10 @@ fun BrowseAnimeSourceCompactGrid(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items(animeList.itemCount) { index ->
|
items(
|
||||||
|
count = animeList.itemCount,
|
||||||
|
key = animeList.itemKey { it.value.id },
|
||||||
|
) { index ->
|
||||||
val anime by animeList[index]?.collectAsState() ?: return@items
|
val anime by animeList[index]?.collectAsState() ?: return@items
|
||||||
BrowseAnimeSourceCompactGridItem(
|
BrowseAnimeSourceCompactGridItem(
|
||||||
anime = anime,
|
anime = anime,
|
||||||
|
|
|
@ -7,6 +7,7 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.paging.LoadState
|
import androidx.paging.LoadState
|
||||||
import androidx.paging.compose.LazyPagingItems
|
import androidx.paging.compose.LazyPagingItems
|
||||||
|
import androidx.paging.compose.itemKey
|
||||||
import androidx.paging.compose.items
|
import androidx.paging.compose.items
|
||||||
import eu.kanade.presentation.browse.InLibraryBadge
|
import eu.kanade.presentation.browse.InLibraryBadge
|
||||||
import eu.kanade.presentation.browse.manga.components.BrowseSourceLoadingItem
|
import eu.kanade.presentation.browse.manga.components.BrowseSourceLoadingItem
|
||||||
|
@ -34,9 +35,11 @@ fun BrowseAnimeSourceList(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items(animeList) { animeflow ->
|
items(
|
||||||
animeflow ?: return@items
|
count = animeList.itemCount,
|
||||||
val anime by animeflow.collectAsState()
|
key = animeList.itemKey { it.value.id },
|
||||||
|
) { index ->
|
||||||
|
val anime by animeList[index]?.collectAsState() ?: return@items
|
||||||
BrowseAnimeSourceListItem(
|
BrowseAnimeSourceListItem(
|
||||||
anime = anime,
|
anime = anime,
|
||||||
onClick = { onAnimeClick(anime) },
|
onClick = { onAnimeClick(anime) },
|
||||||
|
|
|
@ -64,7 +64,7 @@ private fun SourcesFilterContent(
|
||||||
state.items.forEach { (language, sources) ->
|
state.items.forEach { (language, sources) ->
|
||||||
val enabled = language in state.enabledLanguages
|
val enabled = language in state.enabledLanguages
|
||||||
item(
|
item(
|
||||||
key = language.hashCode(),
|
key = language,
|
||||||
contentType = "source-filter-header",
|
contentType = "source-filter-header",
|
||||||
) {
|
) {
|
||||||
SourcesFilterHeader(
|
SourcesFilterHeader(
|
||||||
|
@ -74,18 +74,19 @@ private fun SourcesFilterContent(
|
||||||
onClickItem = onClickLanguage,
|
onClickItem = onClickLanguage,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (!enabled) return@forEach
|
if (enabled) {
|
||||||
items(
|
items(
|
||||||
items = sources,
|
items = sources,
|
||||||
key = { "source-filter-${it.key()}" },
|
key = { "source-filter-${it.key()}" },
|
||||||
contentType = { "source-filter-item" },
|
contentType = { "source-filter-item" },
|
||||||
) { source ->
|
) { source ->
|
||||||
SourcesFilterItem(
|
SourcesFilterItem(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemPlacement(),
|
||||||
source = source,
|
source = source,
|
||||||
enabled = "${source.id}" !in state.disabledSources,
|
enabled = "${source.id}" !in state.disabledSources,
|
||||||
onClickItem = onClickSource,
|
onClickItem = onClickSource,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.paging.LoadState
|
import androidx.paging.LoadState
|
||||||
import androidx.paging.compose.LazyPagingItems
|
import androidx.paging.compose.LazyPagingItems
|
||||||
|
import androidx.paging.compose.itemKey
|
||||||
import eu.kanade.presentation.browse.InLibraryBadge
|
import eu.kanade.presentation.browse.InLibraryBadge
|
||||||
import eu.kanade.presentation.library.CommonEntryItemDefaults
|
import eu.kanade.presentation.library.CommonEntryItemDefaults
|
||||||
import eu.kanade.presentation.library.EntryComfortableGridItem
|
import eu.kanade.presentation.library.EntryComfortableGridItem
|
||||||
|
@ -39,7 +40,10 @@ fun BrowseMangaSourceComfortableGrid(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items(mangaList.itemCount) { index ->
|
items(
|
||||||
|
count = mangaList.itemCount,
|
||||||
|
key = mangaList.itemKey { it.value.id },
|
||||||
|
) { index ->
|
||||||
val manga by mangaList[index]?.collectAsState() ?: return@items
|
val manga by mangaList[index]?.collectAsState() ?: return@items
|
||||||
BrowseMangaSourceComfortableGridItem(
|
BrowseMangaSourceComfortableGridItem(
|
||||||
manga = manga,
|
manga = manga,
|
||||||
|
|
|
@ -11,6 +11,7 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.paging.LoadState
|
import androidx.paging.LoadState
|
||||||
import androidx.paging.compose.LazyPagingItems
|
import androidx.paging.compose.LazyPagingItems
|
||||||
|
import androidx.paging.compose.itemKey
|
||||||
import eu.kanade.presentation.browse.InLibraryBadge
|
import eu.kanade.presentation.browse.InLibraryBadge
|
||||||
import eu.kanade.presentation.library.CommonEntryItemDefaults
|
import eu.kanade.presentation.library.CommonEntryItemDefaults
|
||||||
import eu.kanade.presentation.library.EntryCompactGridItem
|
import eu.kanade.presentation.library.EntryCompactGridItem
|
||||||
|
@ -39,7 +40,10 @@ fun BrowseMangaSourceCompactGrid(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items(mangaList.itemCount) { index ->
|
items(
|
||||||
|
count = mangaList.itemCount,
|
||||||
|
key = mangaList.itemKey { it.value.id },
|
||||||
|
) { index ->
|
||||||
val manga by mangaList[index]?.collectAsState() ?: return@items
|
val manga by mangaList[index]?.collectAsState() ?: return@items
|
||||||
BrowseMangaSourceCompactGridItem(
|
BrowseMangaSourceCompactGridItem(
|
||||||
manga = manga,
|
manga = manga,
|
||||||
|
|
|
@ -7,6 +7,7 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.paging.LoadState
|
import androidx.paging.LoadState
|
||||||
import androidx.paging.compose.LazyPagingItems
|
import androidx.paging.compose.LazyPagingItems
|
||||||
|
import androidx.paging.compose.itemKey
|
||||||
import androidx.paging.compose.items
|
import androidx.paging.compose.items
|
||||||
import eu.kanade.presentation.browse.InLibraryBadge
|
import eu.kanade.presentation.browse.InLibraryBadge
|
||||||
import eu.kanade.presentation.library.CommonEntryItemDefaults
|
import eu.kanade.presentation.library.CommonEntryItemDefaults
|
||||||
|
@ -33,9 +34,11 @@ fun BrowseMangaSourceList(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items(mangaList) { mangaflow ->
|
items(
|
||||||
mangaflow ?: return@items
|
count = mangaList.itemCount,
|
||||||
val manga by mangaflow.collectAsState()
|
key = mangaList.itemKey { it.value.id },
|
||||||
|
) { index ->
|
||||||
|
val manga by mangaList[index]?.collectAsState() ?: return@items
|
||||||
BrowseMangaSourceListItem(
|
BrowseMangaSourceListItem(
|
||||||
manga = manga,
|
manga = manga,
|
||||||
onClick = { onMangaClick(manga) },
|
onClick = { onMangaClick(manga) },
|
||||||
|
|
|
@ -119,12 +119,13 @@ private fun ColumnScope.FilterPage(
|
||||||
onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompletedAnime) },
|
onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompletedAnime) },
|
||||||
)
|
)
|
||||||
|
|
||||||
when (screenModel.trackServices.size) {
|
val trackServices = remember { screenModel.trackServices }
|
||||||
|
when (trackServices.size) {
|
||||||
0 -> {
|
0 -> {
|
||||||
// No trackers
|
// No trackers
|
||||||
}
|
}
|
||||||
1 -> {
|
1 -> {
|
||||||
val service = screenModel.trackServices[0]
|
val service = trackServices[0]
|
||||||
val filterTracker by screenModel.libraryPreferences.filterTrackedAnime(service.id.toInt()).collectAsState()
|
val filterTracker by screenModel.libraryPreferences.filterTrackedAnime(service.id.toInt()).collectAsState()
|
||||||
TriStateItem(
|
TriStateItem(
|
||||||
label = stringResource(R.string.action_filter_tracked),
|
label = stringResource(R.string.action_filter_tracked),
|
||||||
|
@ -134,7 +135,7 @@ private fun ColumnScope.FilterPage(
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
HeadingItem(R.string.action_filter_tracked)
|
HeadingItem(R.string.action_filter_tracked)
|
||||||
screenModel.trackServices.map { service ->
|
trackServices.map { service ->
|
||||||
val filterTracker by screenModel.libraryPreferences.filterTrackedAnime(service.id.toInt()).collectAsState()
|
val filterTracker by screenModel.libraryPreferences.filterTrackedAnime(service.id.toInt()).collectAsState()
|
||||||
TriStateItem(
|
TriStateItem(
|
||||||
label = stringResource(service.nameRes()),
|
label = stringResource(service.nameRes()),
|
||||||
|
|
|
@ -119,12 +119,13 @@ private fun ColumnScope.FilterPage(
|
||||||
onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompletedManga) },
|
onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompletedManga) },
|
||||||
)
|
)
|
||||||
|
|
||||||
when (screenModel.trackServices.size) {
|
val trackServices = remember { screenModel.trackServices }
|
||||||
|
when (trackServices.size) {
|
||||||
0 -> {
|
0 -> {
|
||||||
// No trackers
|
// No trackers
|
||||||
}
|
}
|
||||||
1 -> {
|
1 -> {
|
||||||
val service = screenModel.trackServices[0]
|
val service = trackServices[0]
|
||||||
val filterTracker by screenModel.libraryPreferences.filterTrackedManga(service.id.toInt()).collectAsState()
|
val filterTracker by screenModel.libraryPreferences.filterTrackedManga(service.id.toInt()).collectAsState()
|
||||||
TriStateItem(
|
TriStateItem(
|
||||||
label = stringResource(R.string.action_filter_tracked),
|
label = stringResource(R.string.action_filter_tracked),
|
||||||
|
@ -134,7 +135,7 @@ private fun ColumnScope.FilterPage(
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
HeadingItem(R.string.action_filter_tracked)
|
HeadingItem(R.string.action_filter_tracked)
|
||||||
screenModel.trackServices.map { service ->
|
trackServices.map { service ->
|
||||||
val filterTracker by screenModel.libraryPreferences.filterTrackedManga(service.id.toInt()).collectAsState()
|
val filterTracker by screenModel.libraryPreferences.filterTrackedManga(service.id.toInt()).collectAsState()
|
||||||
TriStateItem(
|
TriStateItem(
|
||||||
label = stringResource(service.nameRes()),
|
label = stringResource(service.nameRes()),
|
||||||
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
package eu.kanade.presentation.reader
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.SkipNext
|
||||||
|
import androidx.compose.material.icons.outlined.SkipPrevious
|
||||||
|
import androidx.compose.material3.FilledIconButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Slider
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import eu.kanade.presentation.util.isTabletUi
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChapterNavigator(
|
||||||
|
isRtl: Boolean,
|
||||||
|
onNextChapter: () -> Unit,
|
||||||
|
enabledNext: Boolean,
|
||||||
|
onPreviousChapter: () -> Unit,
|
||||||
|
enabledPrevious: Boolean,
|
||||||
|
currentPage: Int,
|
||||||
|
totalPages: Int,
|
||||||
|
onSliderValueChange: (Int) -> Unit,
|
||||||
|
) {
|
||||||
|
val isTabletUi = isTabletUi()
|
||||||
|
val horizontalPadding = if (isTabletUi) 24.dp else 16.dp
|
||||||
|
val layoutDirection = if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
|
||||||
|
|
||||||
|
val backgroundColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.9f)
|
||||||
|
val haptic = LocalHapticFeedback.current
|
||||||
|
|
||||||
|
// We explicitly handle direction based on the reader viewer rather than the system direction
|
||||||
|
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = horizontalPadding),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
val isLeftEnabled = if (isRtl) enabledNext else enabledPrevious
|
||||||
|
if (isLeftEnabled) {
|
||||||
|
FilledIconButton(
|
||||||
|
onClick = if (isRtl) onNextChapter else onPreviousChapter,
|
||||||
|
colors = IconButtonDefaults.filledIconButtonColors(
|
||||||
|
containerColor = backgroundColor,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.SkipPrevious,
|
||||||
|
contentDescription = stringResource(if (isRtl) R.string.action_next_chapter else R.string.action_previous_chapter),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalPages > 1) {
|
||||||
|
CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.clip(RoundedCornerShape(24.dp))
|
||||||
|
.background(backgroundColor)
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(text = currentPage.toString())
|
||||||
|
|
||||||
|
Slider(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(horizontal = 8.dp),
|
||||||
|
value = currentPage.toFloat(),
|
||||||
|
valueRange = 1f..totalPages.toFloat(),
|
||||||
|
steps = totalPages,
|
||||||
|
onValueChange = {
|
||||||
|
onSliderValueChange(it.toInt() - 1)
|
||||||
|
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(text = totalPages.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
|
||||||
|
val isRightEnabled = if (isRtl) enabledPrevious else enabledNext
|
||||||
|
if (isRightEnabled) {
|
||||||
|
FilledIconButton(
|
||||||
|
onClick = if (isRtl) onPreviousChapter else onNextChapter,
|
||||||
|
colors = IconButtonDefaults.filledIconButtonColors(
|
||||||
|
containerColor = backgroundColor,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.SkipNext,
|
||||||
|
contentDescription = stringResource(if (isRtl) R.string.action_previous_chapter else R.string.action_next_chapter),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
package eu.kanade.presentation.reader
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
|
import androidx.compose.ui.text.ExperimentalTextApi
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTextApi::class)
|
||||||
|
@Composable
|
||||||
|
fun PageIndicatorText(
|
||||||
|
currentPage: Int,
|
||||||
|
totalPages: Int,
|
||||||
|
) {
|
||||||
|
if (currentPage <= 0 || totalPages <= 0) return
|
||||||
|
|
||||||
|
val text = "$currentPage / $totalPages"
|
||||||
|
|
||||||
|
Box {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
color = Color(45, 45, 45),
|
||||||
|
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
letterSpacing = 1.sp,
|
||||||
|
style = TextStyle.Default.copy(
|
||||||
|
drawStyle = Stroke(width = 4f),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
color = Color(235, 235, 235),
|
||||||
|
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
letterSpacing = 1.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -225,7 +225,6 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
val skippedUpdates = CopyOnWriteArrayList<Pair<Anime, String?>>()
|
val skippedUpdates = CopyOnWriteArrayList<Pair<Anime, String?>>()
|
||||||
val failedUpdates = CopyOnWriteArrayList<Pair<Anime, String?>>()
|
val failedUpdates = CopyOnWriteArrayList<Pair<Anime, String?>>()
|
||||||
val hasDownloads = AtomicBoolean(false)
|
val hasDownloads = AtomicBoolean(false)
|
||||||
val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
|
|
||||||
val restrictions = libraryPreferences.libraryUpdateItemRestriction().get()
|
val restrictions = libraryPreferences.libraryUpdateItemRestriction().get()
|
||||||
|
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
|
@ -290,6 +289,7 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
}
|
}
|
||||||
|
|
||||||
if (libraryPreferences.autoUpdateTrackers().get()) {
|
if (libraryPreferences.autoUpdateTrackers().get()) {
|
||||||
|
val loggedServices = trackManager.services.filter { it.isLogged }
|
||||||
updateTrackings(anime, loggedServices)
|
updateTrackings(anime, loggedServices)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -225,7 +225,6 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
val skippedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
val skippedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
||||||
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
||||||
val hasDownloads = AtomicBoolean(false)
|
val hasDownloads = AtomicBoolean(false)
|
||||||
val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
|
|
||||||
val restrictions = libraryPreferences.libraryUpdateItemRestriction().get()
|
val restrictions = libraryPreferences.libraryUpdateItemRestriction().get()
|
||||||
|
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
|
@ -290,6 +289,7 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
}
|
}
|
||||||
|
|
||||||
if (libraryPreferences.autoUpdateTrackers().get()) {
|
if (libraryPreferences.autoUpdateTrackers().get()) {
|
||||||
|
val loggedServices = trackManager.services.filter { it.isLogged }
|
||||||
updateTrackings(manga, loggedServices)
|
updateTrackings(manga, loggedServices)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import kotlinx.coroutines.launch
|
||||||
import tachiyomi.domain.source.anime.model.AnimeSource
|
import tachiyomi.domain.source.anime.model.AnimeSource
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.util.SortedMap
|
||||||
|
|
||||||
class AnimeSourcesFilterScreenModel(
|
class AnimeSourcesFilterScreenModel(
|
||||||
private val preferences: SourcePreferences = Injekt.get(),
|
private val preferences: SourcePreferences = Injekt.get(),
|
||||||
|
@ -66,7 +67,7 @@ sealed class AnimeSourcesFilterState {
|
||||||
) : AnimeSourcesFilterState()
|
) : AnimeSourcesFilterState()
|
||||||
|
|
||||||
data class Success(
|
data class Success(
|
||||||
val items: Map<String, List<AnimeSource>>,
|
val items: SortedMap<String, List<AnimeSource>>,
|
||||||
val enabledLanguages: Set<String>,
|
val enabledLanguages: Set<String>,
|
||||||
val disabledSources: Set<String>,
|
val disabledSources: Set<String>,
|
||||||
) : AnimeSourcesFilterState() {
|
) : AnimeSourcesFilterState() {
|
||||||
|
|
|
@ -14,6 +14,7 @@ import kotlinx.coroutines.launch
|
||||||
import tachiyomi.domain.source.manga.model.Source
|
import tachiyomi.domain.source.manga.model.Source
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.util.SortedMap
|
||||||
|
|
||||||
class SourcesFilterScreenModel(
|
class SourcesFilterScreenModel(
|
||||||
private val preferences: SourcePreferences = Injekt.get(),
|
private val preferences: SourcePreferences = Injekt.get(),
|
||||||
|
@ -66,7 +67,7 @@ sealed class MangaSourcesFilterState {
|
||||||
) : MangaSourcesFilterState()
|
) : MangaSourcesFilterState()
|
||||||
|
|
||||||
data class Success(
|
data class Success(
|
||||||
val items: Map<String, List<Source>>,
|
val items: SortedMap<String, List<Source>>,
|
||||||
val enabledLanguages: Set<String>,
|
val enabledLanguages: Set<String>,
|
||||||
val disabledSources: Set<String>,
|
val disabledSources: Set<String>,
|
||||||
) : MangaSourcesFilterState() {
|
) : MangaSourcesFilterState() {
|
||||||
|
|
|
@ -628,6 +628,9 @@ class AnimeInfoScreenModel(
|
||||||
downloadEpisodes(episodes, false, video)
|
downloadEpisodes(episodes, false, video)
|
||||||
}
|
}
|
||||||
if (!isFavorited && !successState.hasPromptedToAddBefore) {
|
if (!isFavorited && !successState.hasPromptedToAddBefore) {
|
||||||
|
updateSuccessState { successState ->
|
||||||
|
successState.copy(hasPromptedToAddBefore = true)
|
||||||
|
}
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
val result = snackbarHostState.showSnackbar(
|
val result = snackbarHostState.showSnackbar(
|
||||||
message = context.getString(R.string.snack_add_to_anime_library),
|
message = context.getString(R.string.snack_add_to_anime_library),
|
||||||
|
@ -637,9 +640,6 @@ class AnimeInfoScreenModel(
|
||||||
if (result == SnackbarResult.ActionPerformed && !isFavorited) {
|
if (result == SnackbarResult.ActionPerformed && !isFavorited) {
|
||||||
toggleFavorite()
|
toggleFavorite()
|
||||||
}
|
}
|
||||||
updateSuccessState { successState ->
|
|
||||||
successState.copy(hasPromptedToAddBefore = true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -622,6 +622,9 @@ class MangaInfoScreenModel(
|
||||||
downloadChapters(chapters)
|
downloadChapters(chapters)
|
||||||
}
|
}
|
||||||
if (!isFavorited && !successState.hasPromptedToAddBefore) {
|
if (!isFavorited && !successState.hasPromptedToAddBefore) {
|
||||||
|
updateSuccessState { successState ->
|
||||||
|
successState.copy(hasPromptedToAddBefore = true)
|
||||||
|
}
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
val result = snackbarHostState.showSnackbar(
|
val result = snackbarHostState.showSnackbar(
|
||||||
message = context.getString(R.string.snack_add_to_manga_library),
|
message = context.getString(R.string.snack_add_to_manga_library),
|
||||||
|
@ -631,9 +634,6 @@ class MangaInfoScreenModel(
|
||||||
if (result == SnackbarResult.ActionPerformed && !isFavorited) {
|
if (result == SnackbarResult.ActionPerformed && !isFavorited) {
|
||||||
toggleFavorite()
|
toggleFavorite()
|
||||||
}
|
}
|
||||||
updateSuccessState { successState ->
|
|
||||||
successState.copy(hasPromptedToAddBefore = true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,10 +25,11 @@ class AnimeLibrarySettingsScreenModel(
|
||||||
private val getCategories: GetAnimeCategories = Injekt.get(),
|
private val getCategories: GetAnimeCategories = Injekt.get(),
|
||||||
private val setDisplayModeForCategory: SetDisplayModeForAnimeCategory = Injekt.get(),
|
private val setDisplayModeForCategory: SetDisplayModeForAnimeCategory = Injekt.get(),
|
||||||
private val setSortModeForCategory: SetSortModeForAnimeCategory = Injekt.get(),
|
private val setSortModeForCategory: SetSortModeForAnimeCategory = Injekt.get(),
|
||||||
trackManager: TrackManager = Injekt.get(),
|
private val trackManager: TrackManager = Injekt.get(),
|
||||||
) : ScreenModel {
|
) : ScreenModel {
|
||||||
|
|
||||||
val trackServices = trackManager.services.filter { service -> service.isLogged }
|
val trackServices
|
||||||
|
get() = trackManager.services.filter { it.isLogged }
|
||||||
|
|
||||||
fun togglePreference(preference: (LibraryPreferences) -> Preference<Boolean>) {
|
fun togglePreference(preference: (LibraryPreferences) -> Preference<Boolean>) {
|
||||||
preference(libraryPreferences).toggle()
|
preference(libraryPreferences).toggle()
|
||||||
|
|
|
@ -25,10 +25,11 @@ class MangaLibrarySettingsScreenModel(
|
||||||
private val getCategories: GetMangaCategories = Injekt.get(),
|
private val getCategories: GetMangaCategories = Injekt.get(),
|
||||||
private val setDisplayModeForCategory: SetDisplayModeForMangaCategory = Injekt.get(),
|
private val setDisplayModeForCategory: SetDisplayModeForMangaCategory = Injekt.get(),
|
||||||
private val setSortModeForCategory: SetSortModeForMangaCategory = Injekt.get(),
|
private val setSortModeForCategory: SetSortModeForMangaCategory = Injekt.get(),
|
||||||
trackManager: TrackManager = Injekt.get(),
|
private val trackManager: TrackManager = Injekt.get(),
|
||||||
) : ScreenModel {
|
) : ScreenModel {
|
||||||
|
|
||||||
val trackServices = trackManager.services.filter { service -> service.isLogged }
|
val trackServices
|
||||||
|
get() = trackManager.services.filter { it.isLogged }
|
||||||
|
|
||||||
fun togglePreference(preference: (LibraryPreferences) -> Preference<Boolean>) {
|
fun togglePreference(preference: (LibraryPreferences) -> Preference<Boolean>) {
|
||||||
preference(libraryPreferences).toggle()
|
preference(libraryPreferences).toggle()
|
||||||
|
|
|
@ -1,52 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.ui.reader
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.text.Spannable
|
|
||||||
import android.text.SpannableString
|
|
||||||
import android.text.style.ScaleXSpan
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import androidx.appcompat.widget.AppCompatTextView
|
|
||||||
import eu.kanade.tachiyomi.widget.OutlineSpan
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page indicator found at the bottom of the reader
|
|
||||||
*/
|
|
||||||
class PageIndicatorTextView(
|
|
||||||
context: Context,
|
|
||||||
attrs: AttributeSet? = null,
|
|
||||||
) : AppCompatTextView(context, attrs) {
|
|
||||||
|
|
||||||
init {
|
|
||||||
setTextColor(fillColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
|
||||||
override fun setText(text: CharSequence?, type: BufferType?) {
|
|
||||||
// Add spaces at the start & end of the text, otherwise the stroke is cut-off because it's
|
|
||||||
// not taken into account when measuring the text (view's padding doesn't help).
|
|
||||||
val currText = " $text "
|
|
||||||
|
|
||||||
// Also add a bit of spacing between each character, as the stroke overlaps them
|
|
||||||
val finalText = SpannableString(currText.asIterable().joinToString("\u00A0")).apply {
|
|
||||||
// Apply text outline
|
|
||||||
setSpan(spanOutline, 1, length - 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
||||||
|
|
||||||
for (i in 1..lastIndex step 2) {
|
|
||||||
setSpan(ScaleXSpan(0.2f), i, i + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
super.setText(finalText, BufferType.SPANNABLE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val fillColor = Color.rgb(235, 235, 235)
|
|
||||||
private val strokeColor = Color.rgb(45, 45, 45)
|
|
||||||
|
|
||||||
// A span object with text outlining properties
|
|
||||||
private val spanOutline = OutlineSpan(
|
|
||||||
strokeColor = strokeColor,
|
|
||||||
strokeWidth = 4f,
|
|
||||||
)
|
|
|
@ -6,18 +6,15 @@ import android.app.ProgressDialog
|
||||||
import android.app.assist.AssistContent
|
import android.app.assist.AssistContent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.ColorStateList
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.ColorMatrix
|
import android.graphics.ColorMatrix
|
||||||
import android.graphics.ColorMatrixColorFilter
|
import android.graphics.ColorMatrixColorFilter
|
||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
import android.graphics.drawable.RippleDrawable
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.HapticFeedbackConstants
|
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
@ -29,23 +26,25 @@ import android.view.animation.AnimationUtils
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.core.graphics.ColorUtils
|
import androidx.core.graphics.ColorUtils
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.core.transition.doOnEnd
|
import androidx.core.transition.doOnEnd
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
import androidx.core.view.isInvisible
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||||
import com.google.android.material.shape.MaterialShapeDrawable
|
import com.google.android.material.shape.MaterialShapeDrawable
|
||||||
import com.google.android.material.slider.Slider
|
|
||||||
import com.google.android.material.transition.platform.MaterialContainerTransform
|
import com.google.android.material.transition.platform.MaterialContainerTransform
|
||||||
import dev.chrisbanes.insetter.applyInsetter
|
import dev.chrisbanes.insetter.applyInsetter
|
||||||
import eu.kanade.domain.base.BasePreferences
|
import eu.kanade.domain.base.BasePreferences
|
||||||
import eu.kanade.domain.entries.manga.model.orientationType
|
import eu.kanade.domain.entries.manga.model.orientationType
|
||||||
|
import eu.kanade.presentation.reader.ChapterNavigator
|
||||||
|
import eu.kanade.presentation.reader.PageIndicatorText
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.core.Constants
|
import eu.kanade.tachiyomi.core.Constants
|
||||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
|
@ -64,20 +63,19 @@ import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsSheet
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsSheet
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
|
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
|
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
|
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
|
||||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
||||||
import eu.kanade.tachiyomi.util.preference.toggle
|
import eu.kanade.tachiyomi.util.preference.toggle
|
||||||
import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale
|
import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale
|
||||||
import eu.kanade.tachiyomi.util.system.createReaderThemeContext
|
import eu.kanade.tachiyomi.util.system.createReaderThemeContext
|
||||||
import eu.kanade.tachiyomi.util.system.getThemeColor
|
|
||||||
import eu.kanade.tachiyomi.util.system.hasDisplayCutout
|
import eu.kanade.tachiyomi.util.system.hasDisplayCutout
|
||||||
import eu.kanade.tachiyomi.util.system.isNightMode
|
import eu.kanade.tachiyomi.util.system.isNightMode
|
||||||
import eu.kanade.tachiyomi.util.system.toShareIntent
|
import eu.kanade.tachiyomi.util.system.toShareIntent
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import eu.kanade.tachiyomi.util.view.copy
|
import eu.kanade.tachiyomi.util.view.copy
|
||||||
import eu.kanade.tachiyomi.util.view.popupMenu
|
import eu.kanade.tachiyomi.util.view.popupMenu
|
||||||
|
import eu.kanade.tachiyomi.util.view.setComposeContent
|
||||||
import eu.kanade.tachiyomi.util.view.setTooltip
|
import eu.kanade.tachiyomi.util.view.setTooltip
|
||||||
import eu.kanade.tachiyomi.widget.listener.SimpleAnimationListener
|
import eu.kanade.tachiyomi.widget.listener.SimpleAnimationListener
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
@ -90,6 +88,7 @@ import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.sample
|
import kotlinx.coroutines.flow.sample
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
|
import org.jsoup.internal.StringUtil.padding
|
||||||
import tachiyomi.core.util.lang.launchIO
|
import tachiyomi.core.util.lang.launchIO
|
||||||
import tachiyomi.core.util.lang.launchNonCancellable
|
import tachiyomi.core.util.lang.launchNonCancellable
|
||||||
import tachiyomi.core.util.lang.withUIContext
|
import tachiyomi.core.util.lang.withUIContext
|
||||||
|
@ -97,7 +96,6 @@ import tachiyomi.core.util.system.logcat
|
||||||
import tachiyomi.domain.entries.manga.model.Manga
|
import tachiyomi.domain.entries.manga.model.Manga
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
class ReaderActivity : BaseActivity() {
|
class ReaderActivity : BaseActivity() {
|
||||||
|
|
||||||
|
@ -109,9 +107,6 @@ class ReaderActivity : BaseActivity() {
|
||||||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val ENABLED_BUTTON_IMAGE_ALPHA = 255
|
|
||||||
private const val DISABLED_BUTTON_IMAGE_ALPHA = 64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val readerPreferences: ReaderPreferences by injectLazy()
|
private val readerPreferences: ReaderPreferences by injectLazy()
|
||||||
|
@ -124,12 +119,6 @@ class ReaderActivity : BaseActivity() {
|
||||||
|
|
||||||
val hasCutout by lazy { hasDisplayCutout() }
|
val hasCutout by lazy { hasDisplayCutout() }
|
||||||
|
|
||||||
/**
|
|
||||||
* Viewer used to display the pages (pager, webtoon, ...).
|
|
||||||
*/
|
|
||||||
var viewer: BaseViewer? = null
|
|
||||||
private set
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the menu is currently visible.
|
* Whether the menu is currently visible.
|
||||||
*/
|
*/
|
||||||
|
@ -251,8 +240,7 @@ class ReaderActivity : BaseActivity() {
|
||||||
*/
|
*/
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
viewer?.destroy()
|
viewModel.state.value.viewer?.destroy()
|
||||||
viewer = null
|
|
||||||
config = null
|
config = null
|
||||||
menuToggleToast?.cancel()
|
menuToggleToast?.cancel()
|
||||||
readingModeToast?.cancel()
|
readingModeToast?.cancel()
|
||||||
|
@ -362,7 +350,7 @@ class ReaderActivity : BaseActivity() {
|
||||||
* Dispatches a key event. If the viewer doesn't handle it, call the default implementation.
|
* Dispatches a key event. If the viewer doesn't handle it, call the default implementation.
|
||||||
*/
|
*/
|
||||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||||
val handled = viewer?.handleKeyEvent(event) ?: false
|
val handled = viewModel.state.value.viewer?.handleKeyEvent(event) ?: false
|
||||||
return handled || super.dispatchKeyEvent(event)
|
return handled || super.dispatchKeyEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -371,7 +359,7 @@ class ReaderActivity : BaseActivity() {
|
||||||
* implementation.
|
* implementation.
|
||||||
*/
|
*/
|
||||||
override fun dispatchGenericMotionEvent(event: MotionEvent): Boolean {
|
override fun dispatchGenericMotionEvent(event: MotionEvent): Boolean {
|
||||||
val handled = viewer?.handleGenericMotionEvent(event) ?: false
|
val handled = viewModel.state.value.viewer?.handleGenericMotionEvent(event) ?: false
|
||||||
return handled || super.dispatchGenericMotionEvent(event)
|
return handled || super.dispatchGenericMotionEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -408,42 +396,35 @@ class ReaderActivity : BaseActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init listeners on bottom menu
|
binding.pageNumber.setComposeContent {
|
||||||
binding.pageSlider.addOnSliderTouchListener(
|
val state by viewModel.state.collectAsState()
|
||||||
object : Slider.OnSliderTouchListener {
|
|
||||||
override fun onStartTrackingTouch(slider: Slider) {
|
|
||||||
isScrollingThroughPages = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStopTrackingTouch(slider: Slider) {
|
PageIndicatorText(
|
||||||
isScrollingThroughPages = false
|
currentPage = state.currentPage,
|
||||||
}
|
totalPages = state.totalPages,
|
||||||
},
|
)
|
||||||
)
|
|
||||||
binding.pageSlider.addOnChangeListener { slider, value, fromUser ->
|
|
||||||
if (viewer != null && fromUser) {
|
|
||||||
isScrollingThroughPages = true
|
|
||||||
moveToPageIndex(value.toInt())
|
|
||||||
slider.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
binding.leftChapter.setOnClickListener {
|
|
||||||
if (viewer != null) {
|
// Init listeners on bottom menu
|
||||||
if (viewer is R2LPagerViewer) {
|
binding.readerNav.setComposeContent {
|
||||||
loadNextChapter()
|
val state by viewModel.state.collectAsState()
|
||||||
} else {
|
|
||||||
loadPreviousChapter()
|
if (state.viewer == null) return@setComposeContent
|
||||||
}
|
val isRtl = state.viewer is R2LPagerViewer
|
||||||
}
|
|
||||||
}
|
ChapterNavigator(
|
||||||
binding.rightChapter.setOnClickListener {
|
isRtl = isRtl,
|
||||||
if (viewer != null) {
|
onNextChapter = ::loadNextChapter,
|
||||||
if (viewer is R2LPagerViewer) {
|
enabledNext = state.viewerChapters?.nextChapter != null,
|
||||||
loadPreviousChapter()
|
onPreviousChapter = ::loadPreviousChapter,
|
||||||
} else {
|
enabledPrevious = state.viewerChapters?.prevChapter != null,
|
||||||
loadNextChapter()
|
currentPage = state.currentPage,
|
||||||
}
|
totalPages = state.totalPages,
|
||||||
}
|
onSliderValueChange = {
|
||||||
|
isScrollingThroughPages = true
|
||||||
|
moveToPageIndex(it)
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
initBottomShortcuts()
|
initBottomShortcuts()
|
||||||
|
@ -454,18 +435,6 @@ class ReaderActivity : BaseActivity() {
|
||||||
}
|
}
|
||||||
binding.toolbarBottom.background = toolbarBackground.copy(this@ReaderActivity)
|
binding.toolbarBottom.background = toolbarBackground.copy(this@ReaderActivity)
|
||||||
|
|
||||||
binding.readerSeekbar.background = toolbarBackground.copy(this@ReaderActivity)?.apply {
|
|
||||||
setCornerSize(999F)
|
|
||||||
}
|
|
||||||
listOf(binding.leftChapter, binding.rightChapter).forEach {
|
|
||||||
it.background = binding.readerSeekbar.background.copy(this)
|
|
||||||
it.foreground = RippleDrawable(
|
|
||||||
ColorStateList.valueOf(getThemeColor(android.R.attr.colorControlHighlight)),
|
|
||||||
null,
|
|
||||||
it.background,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val toolbarColor = ColorUtils.setAlphaComponent(
|
val toolbarColor = ColorUtils.setAlphaComponent(
|
||||||
toolbarBackground.resolvedTintColor,
|
toolbarBackground.resolvedTintColor,
|
||||||
toolbarBackground.alpha,
|
toolbarBackground.alpha,
|
||||||
|
@ -659,7 +628,7 @@ class ReaderActivity : BaseActivity() {
|
||||||
* and the toolbar title.
|
* and the toolbar title.
|
||||||
*/
|
*/
|
||||||
private fun setManga(manga: Manga) {
|
private fun setManga(manga: Manga) {
|
||||||
val prevViewer = viewer
|
val prevViewer = viewModel.state.value.viewer
|
||||||
|
|
||||||
val viewerMode = ReadingModeType.fromPreference(viewModel.getMangaReadingMode(resolveDefault = false))
|
val viewerMode = ReadingModeType.fromPreference(viewModel.getMangaReadingMode(resolveDefault = false))
|
||||||
binding.actionReadingMode.setImageResource(viewerMode.iconRes)
|
binding.actionReadingMode.setImageResource(viewerMode.iconRes)
|
||||||
|
@ -681,7 +650,7 @@ class ReaderActivity : BaseActivity() {
|
||||||
prevViewer.destroy()
|
prevViewer.destroy()
|
||||||
binding.viewerContainer.removeAllViews()
|
binding.viewerContainer.removeAllViews()
|
||||||
}
|
}
|
||||||
viewer = newViewer
|
viewModel.onViewerLoaded(newViewer)
|
||||||
updateViewerInset(readerPreferences.fullscreen().get())
|
updateViewerInset(readerPreferences.fullscreen().get())
|
||||||
binding.viewerContainer.addView(newViewer.getView())
|
binding.viewerContainer.addView(newViewer.getView())
|
||||||
|
|
||||||
|
@ -691,15 +660,6 @@ class ReaderActivity : BaseActivity() {
|
||||||
|
|
||||||
supportActionBar?.title = manga.title
|
supportActionBar?.title = manga.title
|
||||||
|
|
||||||
binding.pageSlider.isRTL = newViewer is R2LPagerViewer
|
|
||||||
if (newViewer is R2LPagerViewer) {
|
|
||||||
binding.leftChapter.setTooltip(R.string.action_next_chapter)
|
|
||||||
binding.rightChapter.setTooltip(R.string.action_previous_chapter)
|
|
||||||
} else {
|
|
||||||
binding.leftChapter.setTooltip(R.string.action_previous_chapter)
|
|
||||||
binding.rightChapter.setTooltip(R.string.action_next_chapter)
|
|
||||||
}
|
|
||||||
|
|
||||||
val loadingIndicatorContext = createReaderThemeContext()
|
val loadingIndicatorContext = createReaderThemeContext()
|
||||||
loadingIndicator = ReaderProgressIndicator(loadingIndicatorContext).apply {
|
loadingIndicator = ReaderProgressIndicator(loadingIndicatorContext).apply {
|
||||||
updateLayoutParams<FrameLayout.LayoutParams> {
|
updateLayoutParams<FrameLayout.LayoutParams> {
|
||||||
|
@ -739,26 +699,9 @@ class ReaderActivity : BaseActivity() {
|
||||||
*/
|
*/
|
||||||
private fun setChapters(viewerChapters: ViewerChapters) {
|
private fun setChapters(viewerChapters: ViewerChapters) {
|
||||||
binding.readerContainer.removeView(loadingIndicator)
|
binding.readerContainer.removeView(loadingIndicator)
|
||||||
viewer?.setChapters(viewerChapters)
|
viewModel.state.value.viewer?.setChapters(viewerChapters)
|
||||||
binding.toolbar.subtitle = viewerChapters.currChapter.chapter.name
|
binding.toolbar.subtitle = viewerChapters.currChapter.chapter.name
|
||||||
|
|
||||||
val currentChapterPageCount = viewerChapters.currChapter.pages?.size ?: 1
|
|
||||||
binding.readerSeekbar.isInvisible = currentChapterPageCount == 1
|
|
||||||
|
|
||||||
val leftChapterObject = if (viewer is R2LPagerViewer) viewerChapters.nextChapter else viewerChapters.prevChapter
|
|
||||||
val rightChapterObject = if (viewer is R2LPagerViewer) viewerChapters.prevChapter else viewerChapters.nextChapter
|
|
||||||
|
|
||||||
if (leftChapterObject == null && rightChapterObject == null) {
|
|
||||||
binding.leftChapter.isVisible = false
|
|
||||||
binding.rightChapter.isVisible = false
|
|
||||||
} else {
|
|
||||||
binding.leftChapter.isEnabled = leftChapterObject != null
|
|
||||||
binding.leftChapter.imageAlpha = if (leftChapterObject != null) ENABLED_BUTTON_IMAGE_ALPHA else DISABLED_BUTTON_IMAGE_ALPHA
|
|
||||||
|
|
||||||
binding.rightChapter.isEnabled = rightChapterObject != null
|
|
||||||
binding.rightChapter.imageAlpha = if (rightChapterObject != null) ENABLED_BUTTON_IMAGE_ALPHA else DISABLED_BUTTON_IMAGE_ALPHA
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invalidate menu to show proper chapter bookmark state
|
// Invalidate menu to show proper chapter bookmark state
|
||||||
invalidateOptionsMenu()
|
invalidateOptionsMenu()
|
||||||
|
|
||||||
|
@ -786,7 +729,7 @@ class ReaderActivity : BaseActivity() {
|
||||||
* other cases are handled with chapter transitions on the viewers and chapter preloading.
|
* other cases are handled with chapter transitions on the viewers and chapter preloading.
|
||||||
*/
|
*/
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
fun setProgressDialog(show: Boolean) {
|
private fun setProgressDialog(show: Boolean) {
|
||||||
progressDialog?.dismiss()
|
progressDialog?.dismiss()
|
||||||
progressDialog = if (show) {
|
progressDialog = if (show) {
|
||||||
ProgressDialog.show(this, null, getString(R.string.loading), true)
|
ProgressDialog.show(this, null, getString(R.string.loading), true)
|
||||||
|
@ -800,7 +743,7 @@ class ReaderActivity : BaseActivity() {
|
||||||
* page is not found.
|
* page is not found.
|
||||||
*/
|
*/
|
||||||
private fun moveToPageIndex(index: Int) {
|
private fun moveToPageIndex(index: Int) {
|
||||||
val viewer = viewer ?: return
|
val viewer = viewModel.state.value.viewer ?: return
|
||||||
val currentChapter = viewModel.getCurrentChapter() ?: return
|
val currentChapter = viewModel.getCurrentChapter() ?: return
|
||||||
val page = currentChapter.pages?.getOrNull(index) ?: return
|
val page = currentChapter.pages?.getOrNull(index) ?: return
|
||||||
viewer.moveToPage(page)
|
viewer.moveToPage(page)
|
||||||
|
@ -835,24 +778,6 @@ class ReaderActivity : BaseActivity() {
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
fun onPageSelected(page: ReaderPage) {
|
fun onPageSelected(page: ReaderPage) {
|
||||||
viewModel.onPageSelected(page)
|
viewModel.onPageSelected(page)
|
||||||
val pages = page.chapter.pages ?: return
|
|
||||||
|
|
||||||
// Set bottom page number
|
|
||||||
binding.pageNumber.text = "${page.number}/${pages.size}"
|
|
||||||
|
|
||||||
// Set page numbers
|
|
||||||
if (viewer !is R2LPagerViewer) {
|
|
||||||
binding.leftPageText.text = "${page.number}"
|
|
||||||
binding.rightPageText.text = "${pages.size}"
|
|
||||||
} else {
|
|
||||||
binding.rightPageText.text = "${page.number}"
|
|
||||||
binding.leftPageText.text = "${pages.size}"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set slider progress
|
|
||||||
binding.pageSlider.isEnabled = pages.size > 1
|
|
||||||
binding.pageSlider.valueTo = max(pages.lastIndex.toFloat(), 1f)
|
|
||||||
binding.pageSlider.value = page.index.toFloat()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -980,7 +905,7 @@ class ReaderActivity : BaseActivity() {
|
||||||
* Updates viewer inset depending on fullscreen reader preferences.
|
* Updates viewer inset depending on fullscreen reader preferences.
|
||||||
*/
|
*/
|
||||||
fun updateViewerInset(fullscreen: Boolean) {
|
fun updateViewerInset(fullscreen: Boolean) {
|
||||||
viewer?.getView()?.applyInsetter {
|
viewModel.state.value.viewer?.getView()?.applyInsetter {
|
||||||
if (!fullscreen) {
|
if (!fullscreen) {
|
||||||
type(navigationBars = true, statusBars = true) {
|
type(navigationBars = true, statusBars = true) {
|
||||||
padding()
|
padding()
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.ui.reader
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import com.google.android.material.slider.Slider
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Slider to show current chapter progress.
|
|
||||||
*/
|
|
||||||
class ReaderSlider @JvmOverloads constructor(
|
|
||||||
context: Context,
|
|
||||||
attrs: AttributeSet? = null,
|
|
||||||
) : Slider(context, attrs) {
|
|
||||||
|
|
||||||
init {
|
|
||||||
stepSize = 1f
|
|
||||||
setLabelFormatter { value ->
|
|
||||||
(value.toInt() + 1).toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the slider should draw from right to left.
|
|
||||||
*/
|
|
||||||
var isRTL: Boolean
|
|
||||||
set(value) {
|
|
||||||
layoutDirection = if (value) LAYOUT_DIRECTION_RTL else LAYOUT_DIRECTION_LTR
|
|
||||||
}
|
|
||||||
get() = layoutDirection == LAYOUT_DIRECTION_RTL
|
|
||||||
}
|
|
|
@ -24,6 +24,7 @@ import eu.kanade.tachiyomi.data.saver.Location
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import eu.kanade.tachiyomi.ui.player.viewer.AspectState.Companion.get
|
||||||
import eu.kanade.tachiyomi.ui.reader.loader.ChapterLoader
|
import eu.kanade.tachiyomi.ui.reader.loader.ChapterLoader
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.InsertPage
|
import eu.kanade.tachiyomi.ui.reader.model.InsertPage
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||||
|
@ -33,6 +34,7 @@ import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.viewer.Viewer
|
||||||
import eu.kanade.tachiyomi.util.chapter.removeDuplicates
|
import eu.kanade.tachiyomi.util.chapter.removeDuplicates
|
||||||
import eu.kanade.tachiyomi.util.editCover
|
import eu.kanade.tachiyomi.util.editCover
|
||||||
import eu.kanade.tachiyomi.util.lang.byteSize
|
import eu.kanade.tachiyomi.util.lang.byteSize
|
||||||
|
@ -396,6 +398,14 @@ class ReaderViewModel(
|
||||||
eventChannel.trySend(Event.ReloadViewerChapters)
|
eventChannel.trySend(Event.ReloadViewerChapters)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onViewerLoaded(viewer: Viewer?) {
|
||||||
|
mutableState.update {
|
||||||
|
it.copy(
|
||||||
|
viewer = viewer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called every time a page changes on the reader. Used to mark the flag of chapters being
|
* Called every time a page changes on the reader. Used to mark the flag of chapters being
|
||||||
* read, update tracking services, enqueue downloaded chapter deletion, and updating the active chapter if this
|
* read, update tracking services, enqueue downloaded chapter deletion, and updating the active chapter if this
|
||||||
|
@ -412,6 +422,11 @@ class ReaderViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save last page read and mark as read if needed
|
// Save last page read and mark as read if needed
|
||||||
|
mutableState.update {
|
||||||
|
it.copy(
|
||||||
|
currentPage = page.index + 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
selectedChapter.chapter.last_page_read = page.index
|
selectedChapter.chapter.last_page_read = page.index
|
||||||
val shouldTrack = !incognitoMode || hasTrackers
|
val shouldTrack = !incognitoMode || hasTrackers
|
||||||
if (selectedChapter.pages?.lastIndex == page.index && shouldTrack) {
|
if (selectedChapter.pages?.lastIndex == page.index && shouldTrack) {
|
||||||
|
@ -874,7 +889,16 @@ class ReaderViewModel(
|
||||||
val manga: Manga? = null,
|
val manga: Manga? = null,
|
||||||
val viewerChapters: ViewerChapters? = null,
|
val viewerChapters: ViewerChapters? = null,
|
||||||
val isLoadingAdjacentChapter: Boolean = false,
|
val isLoadingAdjacentChapter: Boolean = false,
|
||||||
)
|
val currentPage: Int = -1,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Viewer used to display the pages (pager, webtoon, ...).
|
||||||
|
*/
|
||||||
|
val viewer: Viewer? = null,
|
||||||
|
) {
|
||||||
|
val totalPages: Int
|
||||||
|
get() = viewerChapters?.currChapter?.pages?.size ?: -1
|
||||||
|
}
|
||||||
|
|
||||||
sealed class Event {
|
sealed class Event {
|
||||||
object ReloadViewerChapters : Event()
|
object ReloadViewerChapters : Event()
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
package eu.kanade.tachiyomi.ui.reader.loader
|
package eu.kanade.tachiyomi.ui.reader.loader
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.os.Build
|
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||||
|
import org.apache.commons.compress.archivers.zip.ZipFile
|
||||||
|
import org.apache.commons.compress.utils.SeekableInMemoryByteChannel
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.util.zip.ZipInputStream
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loader used to load a chapter from a .zip or .cbz file.
|
* Loader used to load a chapter from a .zip or .cbz file.
|
||||||
|
@ -20,29 +21,21 @@ internal class ZipPageLoader(file: File) : PageLoader() {
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
ZipInputStream(FileInputStream(file)).use { zipInputStream ->
|
ByteArrayOutputStream().use { byteArrayOutputStream ->
|
||||||
generateSequence { zipInputStream.nextEntry }
|
FileInputStream(file).use { it.copyTo(byteArrayOutputStream) }
|
||||||
.filterNot { it.isDirectory }
|
|
||||||
.forEach { entry ->
|
ZipFile(SeekableInMemoryByteChannel(byteArrayOutputStream.toByteArray())).use { zip ->
|
||||||
File(tmpDir, entry.name.substringAfterLast("/"))
|
zip.entries.asSequence()
|
||||||
.also { it.createNewFile() }
|
.filterNot { it.isDirectory }
|
||||||
.outputStream().use { pageOutputStream ->
|
.forEach { entry ->
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
File(tmpDir, entry.name.substringAfterLast("/"))
|
||||||
pageOutputStream.write(zipInputStream.readNBytes(entry.size.toInt()))
|
.also { it.createNewFile() }
|
||||||
} else {
|
.outputStream().use { pageOutputStream ->
|
||||||
val buffer = ByteArray(2048)
|
zip.getInputStream(entry).copyTo(pageOutputStream)
|
||||||
var len: Int
|
pageOutputStream.flush()
|
||||||
while (
|
|
||||||
zipInputStream.read(buffer, 0, buffer.size)
|
|
||||||
.also { len = it } >= 0
|
|
||||||
) {
|
|
||||||
pageOutputStream.write(buffer, 0, len)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
pageOutputStream.flush()
|
}
|
||||||
}
|
}
|
||||||
zipInputStream.closeEntry()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@ class ReaderReadingModeSettings @JvmOverloads constructor(context: Context, attr
|
||||||
|
|
||||||
initGeneralPreferences()
|
initGeneralPreferences()
|
||||||
|
|
||||||
when ((context as ReaderActivity).viewer) {
|
when ((context as ReaderActivity).viewModel.state.value.viewer) {
|
||||||
is PagerViewer -> initPagerPreferences()
|
is PagerViewer -> initPagerPreferences()
|
||||||
is WebtoonViewer -> initWebtoonPreferences()
|
is WebtoonViewer -> initWebtoonPreferences()
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
|
import eu.kanade.tachiyomi.ui.reader.viewer.Viewer
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.L2RPagerViewer
|
import eu.kanade.tachiyomi.ui.reader.viewer.pager.L2RPagerViewer
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
|
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.VerticalPagerViewer
|
import eu.kanade.tachiyomi.ui.reader.viewer.pager.VerticalPagerViewer
|
||||||
|
@ -31,7 +31,7 @@ enum class ReadingModeType(val prefValue: Int, @StringRes val stringRes: Int, @D
|
||||||
|
|
||||||
fun fromSpinner(position: Int?) = values().find { value -> value.prefValue == position } ?: DEFAULT
|
fun fromSpinner(position: Int?) = values().find { value -> value.prefValue == position } ?: DEFAULT
|
||||||
|
|
||||||
fun toViewer(preference: Int?, activity: ReaderActivity): BaseViewer {
|
fun toViewer(preference: Int?, activity: ReaderActivity): Viewer {
|
||||||
return when (fromPreference(preference)) {
|
return when (fromPreference(preference)) {
|
||||||
LEFT_TO_RIGHT -> L2RPagerViewer(activity)
|
LEFT_TO_RIGHT -> L2RPagerViewer(activity)
|
||||||
RIGHT_TO_LEFT -> R2LPagerViewer(activity)
|
RIGHT_TO_LEFT -> R2LPagerViewer(activity)
|
||||||
|
|
|
@ -9,7 +9,7 @@ import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
||||||
/**
|
/**
|
||||||
* Interface for implementing a viewer.
|
* Interface for implementing a viewer.
|
||||||
*/
|
*/
|
||||||
interface BaseViewer {
|
interface Viewer {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the view this viewer uses.
|
* Returns the view this viewer uses.
|
|
@ -17,7 +17,7 @@ import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.InsertPage
|
import eu.kanade.tachiyomi.ui.reader.model.InsertPage
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
|
import eu.kanade.tachiyomi.ui.reader.viewer.Viewer
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.NavigationRegion
|
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.NavigationRegion
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
|
@ -26,10 +26,10 @@ import uy.kohesive.injekt.injectLazy
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementation of a [BaseViewer] to display pages with a [ViewPager].
|
* Implementation of a [Viewer] to display pages with a [ViewPager].
|
||||||
*/
|
*/
|
||||||
@Suppress("LeakingThis")
|
@Suppress("LeakingThis")
|
||||||
abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
|
abstract class PagerViewer(val activity: ReaderActivity) : Viewer {
|
||||||
|
|
||||||
val downloadManager: MangaDownloadManager by injectLazy()
|
val downloadManager: MangaDownloadManager by injectLazy()
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.StencilPage
|
import eu.kanade.tachiyomi.ui.reader.model.StencilPage
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
|
import eu.kanade.tachiyomi.ui.reader.viewer.Viewer
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.NavigationRegion
|
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.NavigationRegion
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
|
@ -30,9 +30,9 @@ import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementation of a [BaseViewer] to display pages with a [RecyclerView].
|
* Implementation of a [Viewer] to display pages with a [RecyclerView].
|
||||||
*/
|
*/
|
||||||
class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = true) : BaseViewer {
|
class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = true) : Viewer {
|
||||||
|
|
||||||
val downloadManager: MangaDownloadManager by injectLazy()
|
val downloadManager: MangaDownloadManager by injectLazy()
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,6 @@ import android.graphics.drawable.Drawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.util.TypedValue
|
|
||||||
import androidx.annotation.AttrRes
|
import androidx.annotation.AttrRes
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.appcompat.view.ContextThemeWrapper
|
import androidx.appcompat.view.ContextThemeWrapper
|
||||||
|
@ -89,19 +88,6 @@ fun Context.hasPermission(permission: String) = PermissionChecker.checkSelfPermi
|
||||||
return color
|
return color
|
||||||
}
|
}
|
||||||
|
|
||||||
@ColorInt fun Context.getThemeColor(attr: Int): Int {
|
|
||||||
val tv = TypedValue()
|
|
||||||
return if (this.theme.resolveAttribute(attr, tv, true)) {
|
|
||||||
if (tv.resourceId != 0) {
|
|
||||||
getColor(tv.resourceId)
|
|
||||||
} else {
|
|
||||||
tv.data
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val Context.powerManager: PowerManager
|
val Context.powerManager: PowerManager
|
||||||
get() = getSystemService()!!
|
get() = getSystemService()!!
|
||||||
|
|
||||||
|
|
|
@ -10,10 +10,18 @@ import java.util.Locale
|
||||||
*/
|
*/
|
||||||
object LocaleHelper {
|
object LocaleHelper {
|
||||||
|
|
||||||
val comparator = compareBy<String>(
|
/**
|
||||||
{ getDisplayName(it) },
|
* Sorts by display name, except keeps the "all" (displayed as "Multi") locale at the top.
|
||||||
{ it == "all" },
|
*/
|
||||||
)
|
val comparator = { a: String, b: String ->
|
||||||
|
if (a == "all") {
|
||||||
|
-1
|
||||||
|
} else if (b == "all") {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
getDisplayName(a).compareTo(getDisplayName(b))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns display name of a string language code.
|
* Returns display name of a string language code.
|
||||||
|
|
|
@ -16,15 +16,11 @@
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:descendantFocusability="blocksDescendants" />
|
android:descendantFocusability="blocksDescendants" />
|
||||||
|
|
||||||
<eu.kanade.tachiyomi.ui.reader.PageIndicatorTextView
|
<androidx.compose.ui.platform.ComposeView
|
||||||
android:id="@+id/page_number"
|
android:id="@+id/page_number"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="bottom|center_horizontal"
|
android:layout_gravity="bottom|center_horizontal" />
|
||||||
android:padding="4dp"
|
|
||||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
|
||||||
android:textDirection="ltr"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
|
@ -63,82 +59,12 @@
|
||||||
android:layout_gravity="bottom"
|
android:layout_gravity="bottom"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<LinearLayout
|
<androidx.compose.ui.platform.ComposeView
|
||||||
android:id="@+id/reader_nav"
|
android:id="@+id/reader_nav"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:layout_marginEnd="8dp"
|
|
||||||
android:layout_marginBottom="16dp"
|
android:layout_marginBottom="16dp"
|
||||||
android:layoutDirection="ltr"
|
android:layoutDirection="ltr" />
|
||||||
android:orientation="horizontal">
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/left_chapter"
|
|
||||||
android:layout_width="48dp"
|
|
||||||
android:layout_height="48dp"
|
|
||||||
android:layout_marginEnd="8dp"
|
|
||||||
android:contentDescription="@string/action_previous_chapter"
|
|
||||||
android:padding="@dimen/screen_edge_margin"
|
|
||||||
app:srcCompat="@drawable/ic_skip_previous_24dp"
|
|
||||||
app:tint="?attr/colorOnSurface" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/reader_seekbar"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:clickable="true"
|
|
||||||
android:paddingStart="8dp"
|
|
||||||
android:paddingEnd="8dp"
|
|
||||||
tools:ignore="KeyboardInaccessibleWidget">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/left_page_text"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:gravity="center"
|
|
||||||
android:minWidth="32dp"
|
|
||||||
android:textColor="?attr/colorOnSurface"
|
|
||||||
android:textSize="15sp"
|
|
||||||
tools:text="1" />
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Wonky way of setting height due to issues with horizontally centering the thumb in Android 5.
|
|
||||||
See https://stackoverflow.com/questions/15701767/android-thumb-is-not-centered-in-seekbar
|
|
||||||
-->
|
|
||||||
<eu.kanade.tachiyomi.ui.reader.ReaderSlider
|
|
||||||
android:id="@+id/page_slider"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:maxHeight="?attr/actionBarSize"
|
|
||||||
android:minHeight="?attr/actionBarSize"
|
|
||||||
app:tickVisible="true"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/right_page_text"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:gravity="center"
|
|
||||||
android:minWidth="32dp"
|
|
||||||
android:textColor="?attr/colorOnSurface"
|
|
||||||
android:textSize="15sp"
|
|
||||||
tools:text="15" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/right_chapter"
|
|
||||||
android:layout_width="48dp"
|
|
||||||
android:layout_height="48dp"
|
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:contentDescription="@string/action_next_chapter"
|
|
||||||
android:padding="@dimen/screen_edge_margin"
|
|
||||||
app:srcCompat="@drawable/ic_skip_next_24dp"
|
|
||||||
app:tint="?attr/colorOnSurface" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/toolbar_bottom"
|
android:id="@+id/toolbar_bottom"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
[versions]
|
[versions]
|
||||||
agp_version = "8.0.0"
|
agp_version = "8.0.1"
|
||||||
lifecycle_version = "2.6.1"
|
lifecycle_version = "2.6.1"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
|
@ -15,7 +15,7 @@ splashscreen = "androidx.core:core-splashscreen:1.0.0-alpha02"
|
||||||
recyclerview = "androidx.recyclerview:recyclerview:1.3.0"
|
recyclerview = "androidx.recyclerview:recyclerview:1.3.0"
|
||||||
viewpager = "androidx.viewpager:viewpager:1.1.0-alpha01"
|
viewpager = "androidx.viewpager:viewpager:1.1.0-alpha01"
|
||||||
glance = "androidx.glance:glance-appwidget:1.0.0-alpha03"
|
glance = "androidx.glance:glance-appwidget:1.0.0-alpha03"
|
||||||
profileinstaller = "androidx.profileinstaller:profileinstaller:1.3.0"
|
profileinstaller = "androidx.profileinstaller:profileinstaller:1.3.1"
|
||||||
mediasession = "androidx.media:media:1.6.0"
|
mediasession = "androidx.media:media:1.6.0"
|
||||||
|
|
||||||
lifecycle-common = { module = "androidx.lifecycle:lifecycle-common", version.ref = "lifecycle_version" }
|
lifecycle-common = { module = "androidx.lifecycle:lifecycle-common", version.ref = "lifecycle_version" }
|
||||||
|
@ -26,7 +26,7 @@ work-runtime = "androidx.work:work-runtime-ktx:2.8.1"
|
||||||
guava = "com.google.guava:guava:31.1-android"
|
guava = "com.google.guava:guava:31.1-android"
|
||||||
|
|
||||||
paging-runtime = "androidx.paging:paging-runtime:3.1.1"
|
paging-runtime = "androidx.paging:paging-runtime:3.1.1"
|
||||||
paging-compose = "androidx.paging:paging-compose:1.0.0-alpha18"
|
paging-compose = "androidx.paging:paging-compose:1.0.0-alpha19"
|
||||||
|
|
||||||
benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.1.1"
|
benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.1.1"
|
||||||
test-ext = "androidx.test.ext:junit-ktx:1.1.5"
|
test-ext = "androidx.test.ext:junit-ktx:1.1.5"
|
||||||
|
|
|
@ -31,6 +31,7 @@ jsoup = "org.jsoup:jsoup:1.15.4"
|
||||||
|
|
||||||
disklrucache = "com.jakewharton:disklrucache:2.0.2"
|
disklrucache = "com.jakewharton:disklrucache:2.0.2"
|
||||||
unifile = "com.github.tachiyomiorg:unifile:17bec43"
|
unifile = "com.github.tachiyomiorg:unifile:17bec43"
|
||||||
|
compress = "org.apache.commons:commons-compress:1.23.0"
|
||||||
junrar = "com.github.junrar:junrar:7.5.4"
|
junrar = "com.github.junrar:junrar:7.5.4"
|
||||||
|
|
||||||
sqlite-framework = { module = "androidx.sqlite:sqlite-framework", version.ref = "sqlite" }
|
sqlite-framework = { module = "androidx.sqlite:sqlite-framework", version.ref = "sqlite" }
|
||||||
|
|
Loading…
Reference in a new issue