Last Commit Merged: 9a10656bf0
This commit is contained in:
LuftVerbot 2023-10-04 20:24:51 +02:00
parent 5a2a3fd080
commit 5ceae3116b
38 changed files with 374 additions and 393 deletions

View file

@ -218,6 +218,7 @@ dependencies {
// Disk
implementation(libs.disklrucache)
implementation(libs.unifile)
implementation(libs.compress)
implementation(libs.junrar)
// Preferences

View file

@ -6,13 +6,14 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import tachiyomi.domain.source.anime.model.AnimeSource
import tachiyomi.domain.source.anime.repository.AnimeSourceRepository
import java.util.SortedMap
class GetLanguagesWithAnimeSources(
private val repository: AnimeSourceRepository,
private val preferences: SourcePreferences,
) {
fun subscribe(): Flow<Map<String, List<AnimeSource>>> {
fun subscribe(): Flow<SortedMap<String, List<AnimeSource>>> {
return combine(
preferences.enabledLanguages().changes(),
preferences.disabledAnimeSources().changes(),
@ -23,7 +24,8 @@ class GetLanguagesWithAnimeSources(
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name },
)
sortedSources.groupBy { it.lang }
sortedSources
.groupBy { it.lang }
.toSortedMap(
compareBy<String> { it !in enabledLanguage }.then(LocaleHelper.comparator),
)

View file

@ -6,13 +6,14 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import tachiyomi.domain.source.manga.model.Source
import tachiyomi.domain.source.manga.repository.MangaSourceRepository
import java.util.SortedMap
class GetLanguagesWithMangaSources(
private val repository: MangaSourceRepository,
private val preferences: SourcePreferences,
) {
fun subscribe(): Flow<Map<String, List<Source>>> {
fun subscribe(): Flow<SortedMap<String, List<Source>>> {
return combine(
preferences.enabledLanguages().changes(),
preferences.disabledMangaSources().changes(),
@ -23,7 +24,8 @@ class GetLanguagesWithMangaSources(
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name },
)
sortedSources.groupBy { it.lang }
sortedSources
.groupBy { it.lang }
.toSortedMap(
compareBy<String> { it !in enabledLanguage }.then(LocaleHelper.comparator),
)

View file

@ -64,7 +64,7 @@ private fun AnimeSourcesFilterContent(
state.items.forEach { (language, sources) ->
val enabled = language in state.enabledLanguages
item(
key = language.hashCode(),
key = language,
contentType = "source-filter-header",
) {
AnimeSourcesFilterHeader(
@ -74,18 +74,19 @@ private fun AnimeSourcesFilterContent(
onClickItem = onClickLanguage,
)
}
if (!enabled) return@forEach
items(
items = sources,
key = { "source-filter-${it.key()}" },
contentType = { "source-filter-item" },
) { source ->
AnimeSourcesFilterItem(
modifier = Modifier.animateItemPlacement(),
source = source,
isEnabled = "${source.id}" !in state.disabledSources,
onClickItem = onClickSource,
)
if (enabled) {
items(
items = sources,
key = { "source-filter-${it.key()}" },
contentType = { "source-filter-item" },
) { source ->
AnimeSourcesFilterItem(
modifier = Modifier.animateItemPlacement(),
source = source,
isEnabled = "${source.id}" !in state.disabledSources,
onClickItem = onClickSource,
)
}
}
}
}

View file

@ -11,6 +11,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.unit.dp
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.itemKey
import eu.kanade.presentation.browse.InLibraryBadge
import eu.kanade.presentation.browse.manga.components.BrowseSourceLoadingItem
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
BrowseAnimeSourceComfortableGridItem(
anime = anime,

View file

@ -11,6 +11,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.unit.dp
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.itemKey
import eu.kanade.presentation.browse.InLibraryBadge
import eu.kanade.presentation.browse.manga.components.BrowseSourceLoadingItem
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
BrowseAnimeSourceCompactGridItem(
anime = anime,

View file

@ -7,6 +7,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.unit.dp
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.itemKey
import androidx.paging.compose.items
import eu.kanade.presentation.browse.InLibraryBadge
import eu.kanade.presentation.browse.manga.components.BrowseSourceLoadingItem
@ -34,9 +35,11 @@ fun BrowseAnimeSourceList(
}
}
items(animeList) { animeflow ->
animeflow ?: return@items
val anime by animeflow.collectAsState()
items(
count = animeList.itemCount,
key = animeList.itemKey { it.value.id },
) { index ->
val anime by animeList[index]?.collectAsState() ?: return@items
BrowseAnimeSourceListItem(
anime = anime,
onClick = { onAnimeClick(anime) },

View file

@ -64,7 +64,7 @@ private fun SourcesFilterContent(
state.items.forEach { (language, sources) ->
val enabled = language in state.enabledLanguages
item(
key = language.hashCode(),
key = language,
contentType = "source-filter-header",
) {
SourcesFilterHeader(
@ -74,18 +74,19 @@ private fun SourcesFilterContent(
onClickItem = onClickLanguage,
)
}
if (!enabled) return@forEach
items(
items = sources,
key = { "source-filter-${it.key()}" },
contentType = { "source-filter-item" },
) { source ->
SourcesFilterItem(
modifier = Modifier.animateItemPlacement(),
source = source,
enabled = "${source.id}" !in state.disabledSources,
onClickItem = onClickSource,
)
if (enabled) {
items(
items = sources,
key = { "source-filter-${it.key()}" },
contentType = { "source-filter-item" },
) { source ->
SourcesFilterItem(
modifier = Modifier.animateItemPlacement(),
source = source,
enabled = "${source.id}" !in state.disabledSources,
onClickItem = onClickSource,
)
}
}
}
}

View file

@ -11,6 +11,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.unit.dp
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.itemKey
import eu.kanade.presentation.browse.InLibraryBadge
import eu.kanade.presentation.library.CommonEntryItemDefaults
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
BrowseMangaSourceComfortableGridItem(
manga = manga,

View file

@ -11,6 +11,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.unit.dp
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.itemKey
import eu.kanade.presentation.browse.InLibraryBadge
import eu.kanade.presentation.library.CommonEntryItemDefaults
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
BrowseMangaSourceCompactGridItem(
manga = manga,

View file

@ -7,6 +7,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.unit.dp
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.itemKey
import androidx.paging.compose.items
import eu.kanade.presentation.browse.InLibraryBadge
import eu.kanade.presentation.library.CommonEntryItemDefaults
@ -33,9 +34,11 @@ fun BrowseMangaSourceList(
}
}
items(mangaList) { mangaflow ->
mangaflow ?: return@items
val manga by mangaflow.collectAsState()
items(
count = mangaList.itemCount,
key = mangaList.itemKey { it.value.id },
) { index ->
val manga by mangaList[index]?.collectAsState() ?: return@items
BrowseMangaSourceListItem(
manga = manga,
onClick = { onMangaClick(manga) },

View file

@ -119,12 +119,13 @@ private fun ColumnScope.FilterPage(
onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompletedAnime) },
)
when (screenModel.trackServices.size) {
val trackServices = remember { screenModel.trackServices }
when (trackServices.size) {
0 -> {
// No trackers
}
1 -> {
val service = screenModel.trackServices[0]
val service = trackServices[0]
val filterTracker by screenModel.libraryPreferences.filterTrackedAnime(service.id.toInt()).collectAsState()
TriStateItem(
label = stringResource(R.string.action_filter_tracked),
@ -134,7 +135,7 @@ private fun ColumnScope.FilterPage(
}
else -> {
HeadingItem(R.string.action_filter_tracked)
screenModel.trackServices.map { service ->
trackServices.map { service ->
val filterTracker by screenModel.libraryPreferences.filterTrackedAnime(service.id.toInt()).collectAsState()
TriStateItem(
label = stringResource(service.nameRes()),

View file

@ -119,12 +119,13 @@ private fun ColumnScope.FilterPage(
onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompletedManga) },
)
when (screenModel.trackServices.size) {
val trackServices = remember { screenModel.trackServices }
when (trackServices.size) {
0 -> {
// No trackers
}
1 -> {
val service = screenModel.trackServices[0]
val service = trackServices[0]
val filterTracker by screenModel.libraryPreferences.filterTrackedManga(service.id.toInt()).collectAsState()
TriStateItem(
label = stringResource(R.string.action_filter_tracked),
@ -134,7 +135,7 @@ private fun ColumnScope.FilterPage(
}
else -> {
HeadingItem(R.string.action_filter_tracked)
screenModel.trackServices.map { service ->
trackServices.map { service ->
val filterTracker by screenModel.libraryPreferences.filterTrackedManga(service.id.toInt()).collectAsState()
TriStateItem(
label = stringResource(service.nameRes()),

View file

@ -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),
)
}
}
}
}
}

View file

@ -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,
)
}
}

View file

@ -225,7 +225,6 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
val skippedUpdates = CopyOnWriteArrayList<Pair<Anime, String?>>()
val failedUpdates = CopyOnWriteArrayList<Pair<Anime, String?>>()
val hasDownloads = AtomicBoolean(false)
val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
val restrictions = libraryPreferences.libraryUpdateItemRestriction().get()
coroutineScope {
@ -290,6 +289,7 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
}
if (libraryPreferences.autoUpdateTrackers().get()) {
val loggedServices = trackManager.services.filter { it.isLogged }
updateTrackings(anime, loggedServices)
}
}

View file

@ -225,7 +225,6 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
val skippedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
val hasDownloads = AtomicBoolean(false)
val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
val restrictions = libraryPreferences.libraryUpdateItemRestriction().get()
coroutineScope {
@ -290,6 +289,7 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
}
if (libraryPreferences.autoUpdateTrackers().get()) {
val loggedServices = trackManager.services.filter { it.isLogged }
updateTrackings(manga, loggedServices)
}
}

View file

@ -14,6 +14,7 @@ import kotlinx.coroutines.launch
import tachiyomi.domain.source.anime.model.AnimeSource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.SortedMap
class AnimeSourcesFilterScreenModel(
private val preferences: SourcePreferences = Injekt.get(),
@ -66,7 +67,7 @@ sealed class AnimeSourcesFilterState {
) : AnimeSourcesFilterState()
data class Success(
val items: Map<String, List<AnimeSource>>,
val items: SortedMap<String, List<AnimeSource>>,
val enabledLanguages: Set<String>,
val disabledSources: Set<String>,
) : AnimeSourcesFilterState() {

View file

@ -14,6 +14,7 @@ import kotlinx.coroutines.launch
import tachiyomi.domain.source.manga.model.Source
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.SortedMap
class SourcesFilterScreenModel(
private val preferences: SourcePreferences = Injekt.get(),
@ -66,7 +67,7 @@ sealed class MangaSourcesFilterState {
) : MangaSourcesFilterState()
data class Success(
val items: Map<String, List<Source>>,
val items: SortedMap<String, List<Source>>,
val enabledLanguages: Set<String>,
val disabledSources: Set<String>,
) : MangaSourcesFilterState() {

View file

@ -628,6 +628,9 @@ class AnimeInfoScreenModel(
downloadEpisodes(episodes, false, video)
}
if (!isFavorited && !successState.hasPromptedToAddBefore) {
updateSuccessState { successState ->
successState.copy(hasPromptedToAddBefore = true)
}
coroutineScope.launch {
val result = snackbarHostState.showSnackbar(
message = context.getString(R.string.snack_add_to_anime_library),
@ -637,9 +640,6 @@ class AnimeInfoScreenModel(
if (result == SnackbarResult.ActionPerformed && !isFavorited) {
toggleFavorite()
}
updateSuccessState { successState ->
successState.copy(hasPromptedToAddBefore = true)
}
}
}
}

View file

@ -622,6 +622,9 @@ class MangaInfoScreenModel(
downloadChapters(chapters)
}
if (!isFavorited && !successState.hasPromptedToAddBefore) {
updateSuccessState { successState ->
successState.copy(hasPromptedToAddBefore = true)
}
coroutineScope.launch {
val result = snackbarHostState.showSnackbar(
message = context.getString(R.string.snack_add_to_manga_library),
@ -631,9 +634,6 @@ class MangaInfoScreenModel(
if (result == SnackbarResult.ActionPerformed && !isFavorited) {
toggleFavorite()
}
updateSuccessState { successState ->
successState.copy(hasPromptedToAddBefore = true)
}
}
}
}

View file

@ -25,10 +25,11 @@ class AnimeLibrarySettingsScreenModel(
private val getCategories: GetAnimeCategories = Injekt.get(),
private val setDisplayModeForCategory: SetDisplayModeForAnimeCategory = Injekt.get(),
private val setSortModeForCategory: SetSortModeForAnimeCategory = Injekt.get(),
trackManager: TrackManager = Injekt.get(),
private val trackManager: TrackManager = Injekt.get(),
) : ScreenModel {
val trackServices = trackManager.services.filter { service -> service.isLogged }
val trackServices
get() = trackManager.services.filter { it.isLogged }
fun togglePreference(preference: (LibraryPreferences) -> Preference<Boolean>) {
preference(libraryPreferences).toggle()

View file

@ -25,10 +25,11 @@ class MangaLibrarySettingsScreenModel(
private val getCategories: GetMangaCategories = Injekt.get(),
private val setDisplayModeForCategory: SetDisplayModeForMangaCategory = Injekt.get(),
private val setSortModeForCategory: SetSortModeForMangaCategory = Injekt.get(),
trackManager: TrackManager = Injekt.get(),
private val trackManager: TrackManager = Injekt.get(),
) : ScreenModel {
val trackServices = trackManager.services.filter { service -> service.isLogged }
val trackServices
get() = trackManager.services.filter { it.isLogged }
fun togglePreference(preference: (LibraryPreferences) -> Preference<Boolean>) {
preference(libraryPreferences).toggle()

View file

@ -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,
)

View file

@ -6,18 +6,15 @@ import android.app.ProgressDialog
import android.app.assist.AssistContent
import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter
import android.graphics.Paint
import android.graphics.drawable.RippleDrawable
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.Gravity
import android.view.HapticFeedbackConstants
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuItem
@ -29,23 +26,25 @@ import android.view.animation.AnimationUtils
import android.widget.FrameLayout
import android.widget.Toast
import androidx.activity.viewModels
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.core.graphics.ColorUtils
import androidx.core.net.toUri
import androidx.core.transition.doOnEnd
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.google.android.material.shape.MaterialShapeDrawable
import com.google.android.material.slider.Slider
import com.google.android.material.transition.platform.MaterialContainerTransform
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.domain.base.BasePreferences
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.core.Constants
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.ReaderSettingsSheet
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.pager.R2LPagerViewer
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.preference.toggle
import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale
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.isNightMode
import eu.kanade.tachiyomi.util.system.toShareIntent
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.copy
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.widget.listener.SimpleAnimationListener
import kotlinx.coroutines.flow.distinctUntilChanged
@ -90,6 +88,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
import logcat.LogPriority
import org.jsoup.internal.StringUtil.padding
import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.lang.launchNonCancellable
import tachiyomi.core.util.lang.withUIContext
@ -97,7 +96,6 @@ import tachiyomi.core.util.system.logcat
import tachiyomi.domain.entries.manga.model.Manga
import uy.kohesive.injekt.injectLazy
import kotlin.math.abs
import kotlin.math.max
class ReaderActivity : BaseActivity() {
@ -109,9 +107,6 @@ class ReaderActivity : BaseActivity() {
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()
@ -124,12 +119,6 @@ class ReaderActivity : BaseActivity() {
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.
*/
@ -251,8 +240,7 @@ class ReaderActivity : BaseActivity() {
*/
override fun onDestroy() {
super.onDestroy()
viewer?.destroy()
viewer = null
viewModel.state.value.viewer?.destroy()
config = null
menuToggleToast?.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.
*/
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)
}
@ -371,7 +359,7 @@ class ReaderActivity : BaseActivity() {
* implementation.
*/
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)
}
@ -408,42 +396,35 @@ class ReaderActivity : BaseActivity() {
}
}
// Init listeners on bottom menu
binding.pageSlider.addOnSliderTouchListener(
object : Slider.OnSliderTouchListener {
override fun onStartTrackingTouch(slider: Slider) {
isScrollingThroughPages = true
}
binding.pageNumber.setComposeContent {
val state by viewModel.state.collectAsState()
override fun onStopTrackingTouch(slider: Slider) {
isScrollingThroughPages = false
}
},
)
binding.pageSlider.addOnChangeListener { slider, value, fromUser ->
if (viewer != null && fromUser) {
isScrollingThroughPages = true
moveToPageIndex(value.toInt())
slider.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
}
PageIndicatorText(
currentPage = state.currentPage,
totalPages = state.totalPages,
)
}
binding.leftChapter.setOnClickListener {
if (viewer != null) {
if (viewer is R2LPagerViewer) {
loadNextChapter()
} else {
loadPreviousChapter()
}
}
}
binding.rightChapter.setOnClickListener {
if (viewer != null) {
if (viewer is R2LPagerViewer) {
loadPreviousChapter()
} else {
loadNextChapter()
}
}
// Init listeners on bottom menu
binding.readerNav.setComposeContent {
val state by viewModel.state.collectAsState()
if (state.viewer == null) return@setComposeContent
val isRtl = state.viewer is R2LPagerViewer
ChapterNavigator(
isRtl = isRtl,
onNextChapter = ::loadNextChapter,
enabledNext = state.viewerChapters?.nextChapter != null,
onPreviousChapter = ::loadPreviousChapter,
enabledPrevious = state.viewerChapters?.prevChapter != null,
currentPage = state.currentPage,
totalPages = state.totalPages,
onSliderValueChange = {
isScrollingThroughPages = true
moveToPageIndex(it)
},
)
}
initBottomShortcuts()
@ -454,18 +435,6 @@ class ReaderActivity : BaseActivity() {
}
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(
toolbarBackground.resolvedTintColor,
toolbarBackground.alpha,
@ -659,7 +628,7 @@ class ReaderActivity : BaseActivity() {
* and the toolbar title.
*/
private fun setManga(manga: Manga) {
val prevViewer = viewer
val prevViewer = viewModel.state.value.viewer
val viewerMode = ReadingModeType.fromPreference(viewModel.getMangaReadingMode(resolveDefault = false))
binding.actionReadingMode.setImageResource(viewerMode.iconRes)
@ -681,7 +650,7 @@ class ReaderActivity : BaseActivity() {
prevViewer.destroy()
binding.viewerContainer.removeAllViews()
}
viewer = newViewer
viewModel.onViewerLoaded(newViewer)
updateViewerInset(readerPreferences.fullscreen().get())
binding.viewerContainer.addView(newViewer.getView())
@ -691,15 +660,6 @@ class ReaderActivity : BaseActivity() {
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()
loadingIndicator = ReaderProgressIndicator(loadingIndicatorContext).apply {
updateLayoutParams<FrameLayout.LayoutParams> {
@ -739,26 +699,9 @@ class ReaderActivity : BaseActivity() {
*/
private fun setChapters(viewerChapters: ViewerChapters) {
binding.readerContainer.removeView(loadingIndicator)
viewer?.setChapters(viewerChapters)
viewModel.state.value.viewer?.setChapters(viewerChapters)
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
invalidateOptionsMenu()
@ -786,7 +729,7 @@ class ReaderActivity : BaseActivity() {
* other cases are handled with chapter transitions on the viewers and chapter preloading.
*/
@Suppress("DEPRECATION")
fun setProgressDialog(show: Boolean) {
private fun setProgressDialog(show: Boolean) {
progressDialog?.dismiss()
progressDialog = if (show) {
ProgressDialog.show(this, null, getString(R.string.loading), true)
@ -800,7 +743,7 @@ class ReaderActivity : BaseActivity() {
* page is not found.
*/
private fun moveToPageIndex(index: Int) {
val viewer = viewer ?: return
val viewer = viewModel.state.value.viewer ?: return
val currentChapter = viewModel.getCurrentChapter() ?: return
val page = currentChapter.pages?.getOrNull(index) ?: return
viewer.moveToPage(page)
@ -835,24 +778,6 @@ class ReaderActivity : BaseActivity() {
@SuppressLint("SetTextI18n")
fun onPageSelected(page: ReaderPage) {
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.
*/
fun updateViewerInset(fullscreen: Boolean) {
viewer?.getView()?.applyInsetter {
viewModel.state.value.viewer?.getView()?.applyInsetter {
if (!fullscreen) {
type(navigationBars = true, statusBars = true) {
padding()

View file

@ -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
}

View file

@ -24,6 +24,7 @@ import eu.kanade.tachiyomi.data.saver.Location
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.model.Page
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.model.InsertPage
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.ReaderPreferences
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.editCover
import eu.kanade.tachiyomi.util.lang.byteSize
@ -396,6 +398,14 @@ class ReaderViewModel(
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
* 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
mutableState.update {
it.copy(
currentPage = page.index + 1,
)
}
selectedChapter.chapter.last_page_read = page.index
val shouldTrack = !incognitoMode || hasTrackers
if (selectedChapter.pages?.lastIndex == page.index && shouldTrack) {
@ -874,7 +889,16 @@ class ReaderViewModel(
val manga: Manga? = null,
val viewerChapters: ViewerChapters? = null,
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 {
object ReloadViewerChapters : Event()

View file

@ -1,12 +1,13 @@
package eu.kanade.tachiyomi.ui.reader.loader
import android.app.Application
import android.os.Build
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 java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileInputStream
import java.util.zip.ZipInputStream
/**
* Loader used to load a chapter from a .zip or .cbz file.
@ -20,29 +21,21 @@ internal class ZipPageLoader(file: File) : PageLoader() {
}
init {
ZipInputStream(FileInputStream(file)).use { zipInputStream ->
generateSequence { zipInputStream.nextEntry }
.filterNot { it.isDirectory }
.forEach { entry ->
File(tmpDir, entry.name.substringAfterLast("/"))
.also { it.createNewFile() }
.outputStream().use { pageOutputStream ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
pageOutputStream.write(zipInputStream.readNBytes(entry.size.toInt()))
} else {
val buffer = ByteArray(2048)
var len: Int
while (
zipInputStream.read(buffer, 0, buffer.size)
.also { len = it } >= 0
) {
pageOutputStream.write(buffer, 0, len)
}
ByteArrayOutputStream().use { byteArrayOutputStream ->
FileInputStream(file).use { it.copyTo(byteArrayOutputStream) }
ZipFile(SeekableInMemoryByteChannel(byteArrayOutputStream.toByteArray())).use { zip ->
zip.entries.asSequence()
.filterNot { it.isDirectory }
.forEach { entry ->
File(tmpDir, entry.name.substringAfterLast("/"))
.also { it.createNewFile() }
.outputStream().use { pageOutputStream ->
zip.getInputStream(entry).copyTo(pageOutputStream)
pageOutputStream.flush()
}
pageOutputStream.flush()
}
zipInputStream.closeEntry()
}
}
}
}
}

View file

@ -34,7 +34,7 @@ class ReaderReadingModeSettings @JvmOverloads constructor(context: Context, attr
initGeneralPreferences()
when ((context as ReaderActivity).viewer) {
when ((context as ReaderActivity).viewModel.state.value.viewer) {
is PagerViewer -> initPagerPreferences()
is WebtoonViewer -> initWebtoonPreferences()
}

View file

@ -4,7 +4,7 @@ import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R
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.R2LPagerViewer
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 toViewer(preference: Int?, activity: ReaderActivity): BaseViewer {
fun toViewer(preference: Int?, activity: ReaderActivity): Viewer {
return when (fromPreference(preference)) {
LEFT_TO_RIGHT -> L2RPagerViewer(activity)
RIGHT_TO_LEFT -> R2LPagerViewer(activity)

View file

@ -9,7 +9,7 @@ import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
/**
* Interface for implementing a viewer.
*/
interface BaseViewer {
interface Viewer {
/**
* Returns the view this viewer uses.

View file

@ -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.ReaderPage
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 kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
@ -26,10 +26,10 @@ import uy.kohesive.injekt.injectLazy
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")
abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
abstract class PagerViewer(val activity: ReaderActivity) : Viewer {
val downloadManager: MangaDownloadManager by injectLazy()

View file

@ -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.ViewerChapters
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 kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
@ -30,9 +30,9 @@ import kotlin.math.max
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()

View file

@ -12,7 +12,6 @@ import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.os.PowerManager
import android.util.TypedValue
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
import androidx.appcompat.view.ContextThemeWrapper
@ -89,19 +88,6 @@ fun Context.hasPermission(permission: String) = PermissionChecker.checkSelfPermi
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
get() = getSystemService()!!

View file

@ -10,10 +10,18 @@ import java.util.Locale
*/
object LocaleHelper {
val comparator = compareBy<String>(
{ getDisplayName(it) },
{ it == "all" },
)
/**
* Sorts by display name, except keeps the "all" (displayed as "Multi") locale at the top.
*/
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.

View file

@ -16,15 +16,11 @@
android:layout_height="match_parent"
android:descendantFocusability="blocksDescendants" />
<eu.kanade.tachiyomi.ui.reader.PageIndicatorTextView
<androidx.compose.ui.platform.ComposeView
android:id="@+id/page_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:padding="4dp"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textDirection="ltr"
android:textStyle="bold" />
android:layout_gravity="bottom|center_horizontal" />
</FrameLayout>
@ -63,82 +59,12 @@
android:layout_gravity="bottom"
android:orientation="vertical">
<LinearLayout
<androidx.compose.ui.platform.ComposeView
android:id="@+id/reader_nav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="16dp"
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>
android:layoutDirection="ltr" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/toolbar_bottom"

View file

@ -1,5 +1,5 @@
[versions]
agp_version = "8.0.0"
agp_version = "8.0.1"
lifecycle_version = "2.6.1"
[libraries]
@ -15,7 +15,7 @@ splashscreen = "androidx.core:core-splashscreen:1.0.0-alpha02"
recyclerview = "androidx.recyclerview:recyclerview:1.3.0"
viewpager = "androidx.viewpager:viewpager:1.1.0-alpha01"
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"
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"
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"
test-ext = "androidx.test.ext:junit-ktx:1.1.5"

View file

@ -31,6 +31,7 @@ jsoup = "org.jsoup:jsoup:1.15.4"
disklrucache = "com.jakewharton:disklrucache:2.0.2"
unifile = "com.github.tachiyomiorg:unifile:17bec43"
compress = "org.apache.commons:commons-compress:1.23.0"
junrar = "com.github.junrar:junrar:7.5.4"
sqlite-framework = { module = "androidx.sqlite:sqlite-framework", version.ref = "sqlite" }