Last commit merged: cf3f2d0380
This commit is contained in:
LuftVerbot 2023-11-03 22:30:37 +01:00
parent dd69ce5a12
commit 264b0e6127
60 changed files with 825 additions and 1054 deletions

View file

@ -19,19 +19,24 @@ import tachiyomi.domain.track.anime.interactor.GetAnimeTracks
import tachiyomi.domain.track.anime.interactor.InsertAnimeTrack
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.concurrent.TimeUnit
import kotlin.time.Duration.Companion.minutes
import kotlin.time.toJavaDuration
class DelayedAnimeTrackingUpdateJob(context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result {
if (runAttemptCount > 3) {
return Result.failure()
}
val getTracks = Injekt.get<GetAnimeTracks>()
val insertTrack = Injekt.get<InsertAnimeTrack>()
val trackManager = Injekt.get<TrackManager>()
val delayedTrackingStore = Injekt.get<DelayedAnimeTrackingStore>()
val results = withIOContext {
withIOContext {
delayedTrackingStore.getAnimeItems()
.mapNotNull {
val track = getTracks.awaitOne(it.trackId)
@ -40,7 +45,7 @@ class DelayedAnimeTrackingUpdateJob(context: Context, workerParams: WorkerParame
}
track?.copy(lastEpisodeSeen = it.lastEpisodeSeen.toDouble())
}
.mapNotNull { animeTrack ->
.forEach { animeTrack ->
try {
val service = trackManager.getService(animeTrack.syncId)
if (service != null && service.isLogged) {
@ -57,7 +62,7 @@ class DelayedAnimeTrackingUpdateJob(context: Context, workerParams: WorkerParame
}
}
return if (results.isNotEmpty()) Result.failure() else Result.success()
return if (delayedTrackingStore.getAnimeItems().isEmpty()) Result.success() else Result.retry()
}
companion object {
@ -70,7 +75,7 @@ class DelayedAnimeTrackingUpdateJob(context: Context, workerParams: WorkerParame
val request = OneTimeWorkRequestBuilder<DelayedAnimeTrackingUpdateJob>()
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 20, TimeUnit.SECONDS)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5.minutes.toJavaDuration())
.addTag(TAG)
.build()

View file

@ -19,19 +19,24 @@ import tachiyomi.domain.track.manga.interactor.GetMangaTracks
import tachiyomi.domain.track.manga.interactor.InsertMangaTrack
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.concurrent.TimeUnit
import kotlin.time.Duration.Companion.minutes
import kotlin.time.toJavaDuration
class DelayedMangaTrackingUpdateJob(context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result {
if (runAttemptCount > 3) {
return Result.failure()
}
val getTracks = Injekt.get<GetMangaTracks>()
val insertTrack = Injekt.get<InsertMangaTrack>()
val trackManager = Injekt.get<TrackManager>()
val delayedTrackingStore = Injekt.get<DelayedMangaTrackingStore>()
val results = withIOContext {
withIOContext {
delayedTrackingStore.getMangaItems()
.mapNotNull {
val track = getTracks.awaitOne(it.trackId)
@ -40,7 +45,7 @@ class DelayedMangaTrackingUpdateJob(context: Context, workerParams: WorkerParame
}
track?.copy(lastChapterRead = it.lastChapterRead.toDouble())
}
.mapNotNull { track ->
.forEach { track ->
try {
val service = trackManager.getService(track.syncId)
if (service != null && service.isLogged) {
@ -57,7 +62,7 @@ class DelayedMangaTrackingUpdateJob(context: Context, workerParams: WorkerParame
}
}
return if (results.isNotEmpty()) Result.failure() else Result.success()
return if (delayedTrackingStore.getMangaItems().isEmpty()) Result.success() else Result.retry()
}
companion object {
@ -70,7 +75,7 @@ class DelayedMangaTrackingUpdateJob(context: Context, workerParams: WorkerParame
val request = OneTimeWorkRequestBuilder<DelayedMangaTrackingUpdateJob>()
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 20, TimeUnit.SECONDS)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5.minutes.toJavaDuration())
.addTag(TAG)
.build()

View file

@ -292,7 +292,7 @@ private fun AnimeScreenSmallImpl(
) {
val episodeListState = rememberLazyListState()
val episodes = remember(state) { state.processedEpisodes.toList() }
val episodes = remember(state) { state.processedEpisodes }
val internalOnBackPressed = {
if (episodes.fastAny { it.selected }) {
@ -358,7 +358,7 @@ private fun AnimeScreenSmallImpl(
) {
ExtendedFloatingActionButton(
text = {
val id = if (episodes.fastAny { it.episode.seen }) {
val id = if (state.episodes.fastAny { it.episode.seen }) {
R.string.action_resume
} else {
R.string.action_start
@ -559,7 +559,7 @@ fun AnimeScreenLargeImpl(
val layoutDirection = LocalLayoutDirection.current
val density = LocalDensity.current
val episodes = remember(state) { state.processedEpisodes.toList() }
val episodes = remember(state) { state.processedEpisodes }
val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
var topBarHeight by remember { mutableIntStateOf(0) }
@ -635,7 +635,7 @@ fun AnimeScreenLargeImpl(
) {
ExtendedFloatingActionButton(
text = {
val id = if (episodes.fastAny { it.episode.seen }) {
val id = if (state.episodes.fastAny { it.episode.seen }) {
R.string.action_resume
} else {
R.string.action_start

View file

@ -18,13 +18,14 @@ import androidx.compose.material.DismissValue
import androidx.compose.material.SwipeToDismiss
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bookmark
import androidx.compose.material.icons.filled.BookmarkRemove
import androidx.compose.material.icons.filled.Circle
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.FileDownloadOff
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material.icons.outlined.BookmarkAdd
import androidx.compose.material.icons.outlined.BookmarkRemove
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.Done
import androidx.compose.material.icons.outlined.Download
import androidx.compose.material.icons.outlined.FileDownloadOff
import androidx.compose.material.icons.outlined.RemoveDone
import androidx.compose.material.rememberDismissState
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
@ -296,32 +297,30 @@ private fun SwipeBackgroundIcon(
val imageVector = when (swipeAction) {
LibraryPreferences.EpisodeSwipeAction.ToggleSeen -> {
if (!seen) {
Icons.Default.Visibility
Icons.Outlined.Done
} else {
Icons.Default.VisibilityOff
Icons.Outlined.RemoveDone
}
}
LibraryPreferences.EpisodeSwipeAction.ToggleBookmark -> {
if (!bookmark) {
Icons.Default.Bookmark
Icons.Outlined.BookmarkAdd
} else {
Icons.Default.BookmarkRemove
Icons.Outlined.BookmarkRemove
}
}
LibraryPreferences.EpisodeSwipeAction.Download -> {
when (downloadState) {
AnimeDownload.State.NOT_DOWNLOADED,
AnimeDownload.State.ERROR,
-> { Icons.Default.Download }
-> { Icons.Outlined.Download }
AnimeDownload.State.QUEUE,
AnimeDownload.State.DOWNLOADING,
-> { Icons.Default.FileDownloadOff }
AnimeDownload.State.DOWNLOADED -> { Icons.Default.Delete }
-> { Icons.Outlined.FileDownloadOff }
AnimeDownload.State.DOWNLOADED -> { Icons.Outlined.Delete }
}
}
LibraryPreferences.EpisodeSwipeAction.Disabled -> {
null
}
LibraryPreferences.EpisodeSwipeAction.Disabled -> null
}
imageVector?.let {
Icon(

View file

@ -272,7 +272,7 @@ private fun MangaScreenSmallImpl(
) {
val chapterListState = rememberLazyListState()
val chapters = remember(state) { state.processedChapters.toList() }
val chapters = remember(state) { state.processedChapters }
val internalOnBackPressed = {
if (chapters.fastAny { it.selected }) {
@ -337,7 +337,7 @@ private fun MangaScreenSmallImpl(
) {
ExtendedFloatingActionButton(
text = {
val id = if (chapters.fastAny { it.chapter.read }) {
val id = if (state.chapters.fastAny { it.chapter.read }) {
R.string.action_resume
} else {
R.string.action_start
@ -504,7 +504,7 @@ fun MangaScreenLargeImpl(
val layoutDirection = LocalLayoutDirection.current
val density = LocalDensity.current
val chapters = remember(state) { state.processedChapters.toList() }
val chapters = remember(state) { state.processedChapters }
val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
var topBarHeight by remember { mutableIntStateOf(0) }
@ -577,7 +577,7 @@ fun MangaScreenLargeImpl(
) {
ExtendedFloatingActionButton(
text = {
val id = if (chapters.fastAny { it.chapter.read }) {
val id = if (state.chapters.fastAny { it.chapter.read }) {
R.string.action_resume
} else {
R.string.action_start

View file

@ -17,13 +17,14 @@ import androidx.compose.material.DismissValue
import androidx.compose.material.SwipeToDismiss
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bookmark
import androidx.compose.material.icons.filled.BookmarkRemove
import androidx.compose.material.icons.filled.Circle
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.FileDownloadOff
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material.icons.outlined.BookmarkAdd
import androidx.compose.material.icons.outlined.BookmarkRemove
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.Done
import androidx.compose.material.icons.outlined.Download
import androidx.compose.material.icons.outlined.FileDownloadOff
import androidx.compose.material.icons.outlined.RemoveDone
import androidx.compose.material.rememberDismissState
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
@ -302,32 +303,30 @@ private fun SwipeBackgroundIcon(
val imageVector = when (swipeAction) {
LibraryPreferences.ChapterSwipeAction.ToggleRead -> {
if (!read) {
Icons.Default.Visibility
Icons.Outlined.Done
} else {
Icons.Default.VisibilityOff
Icons.Outlined.RemoveDone
}
}
LibraryPreferences.ChapterSwipeAction.ToggleBookmark -> {
if (!bookmark) {
Icons.Default.Bookmark
Icons.Outlined.BookmarkAdd
} else {
Icons.Default.BookmarkRemove
Icons.Outlined.BookmarkRemove
}
}
LibraryPreferences.ChapterSwipeAction.Download -> {
when (downloadState) {
MangaDownload.State.NOT_DOWNLOADED,
MangaDownload.State.ERROR,
-> { Icons.Default.Download }
-> { Icons.Outlined.Download }
MangaDownload.State.QUEUE,
MangaDownload.State.DOWNLOADING,
-> { Icons.Default.FileDownloadOff }
MangaDownload.State.DOWNLOADED -> { Icons.Default.Delete }
-> { Icons.Outlined.FileDownloadOff }
MangaDownload.State.DOWNLOADED -> { Icons.Outlined.Delete }
}
}
LibraryPreferences.ChapterSwipeAction.Disabled -> {
null
}
LibraryPreferences.ChapterSwipeAction.Disabled -> null
}
imageVector?.let {
Icon(

View file

@ -1,27 +1,19 @@
package eu.kanade.presentation.library.anime
import android.content.res.Configuration
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.TabbedDialog
import eu.kanade.presentation.components.TabbedDialogPaddings
import eu.kanade.presentation.components.TriStateItem
@ -37,7 +29,7 @@ import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.presentation.core.components.CheckboxItem
import tachiyomi.presentation.core.components.HeadingItem
import tachiyomi.presentation.core.components.RadioItem
import tachiyomi.presentation.core.components.SettingsItemsPaddings
import tachiyomi.presentation.core.components.SliderItem
import tachiyomi.presentation.core.components.SortItem
@Composable
@ -198,48 +190,27 @@ private fun ColumnScope.DisplayPage(
}
if (displayMode != LibraryDisplayMode.List) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(
horizontal = SettingsItemsPaddings.Horizontal,
vertical = SettingsItemsPaddings.Vertical,
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(24.dp),
) {
val configuration = LocalConfiguration.current
val columnPreference = remember {
if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
screenModel.libraryPreferences.animeLandscapeColumns()
} else {
screenModel.libraryPreferences.animePortraitColumns()
}
val configuration = LocalConfiguration.current
val columnPreference = remember {
if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
screenModel.libraryPreferences.animeLandscapeColumns()
} else {
screenModel.libraryPreferences.animePortraitColumns()
}
val columns by columnPreference.collectAsState()
Column(modifier = Modifier.weight(0.5f)) {
Text(
stringResource(id = R.string.pref_library_columns),
style = MaterialTheme.typography.bodyMedium,
)
Text(
if (columns > 0) {
stringResource(id = R.string.pref_library_columns_per_row, columns)
} else {
stringResource(id = R.string.label_default)
},
)
}
Slider(
value = columns.toFloat(),
onValueChange = { columnPreference.set(it.toInt()) },
modifier = Modifier.weight(1.5f),
valueRange = 0f..10f,
steps = 10,
)
}
val columns by columnPreference.collectAsState()
SliderItem(
label = stringResource(R.string.pref_library_columns),
max = 10,
value = columns,
valueText = if (columns > 0) {
stringResource(R.string.pref_library_columns_per_row, columns)
} else {
stringResource(R.string.label_default)
},
onChange = { columnPreference.set(it) },
)
}
HeadingItem(R.string.overlay_header)

View file

@ -1,27 +1,19 @@
package eu.kanade.presentation.library.manga
import android.content.res.Configuration
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.TabbedDialog
import eu.kanade.presentation.components.TabbedDialogPaddings
import eu.kanade.presentation.components.TriStateItem
@ -37,7 +29,7 @@ import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.presentation.core.components.CheckboxItem
import tachiyomi.presentation.core.components.HeadingItem
import tachiyomi.presentation.core.components.RadioItem
import tachiyomi.presentation.core.components.SettingsItemsPaddings
import tachiyomi.presentation.core.components.SliderItem
import tachiyomi.presentation.core.components.SortItem
@Composable
@ -197,48 +189,27 @@ private fun ColumnScope.DisplayPage(
}
if (displayMode != LibraryDisplayMode.List) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(
horizontal = SettingsItemsPaddings.Horizontal,
vertical = SettingsItemsPaddings.Vertical,
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(24.dp),
) {
val configuration = LocalConfiguration.current
val columnPreference = remember {
if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
screenModel.libraryPreferences.mangaLandscapeColumns()
} else {
screenModel.libraryPreferences.mangaPortraitColumns()
}
val configuration = LocalConfiguration.current
val columnPreference = remember {
if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
screenModel.libraryPreferences.mangaLandscapeColumns()
} else {
screenModel.libraryPreferences.mangaPortraitColumns()
}
val columns by columnPreference.collectAsState()
Column(modifier = Modifier.weight(0.5f)) {
Text(
stringResource(id = R.string.pref_library_columns),
style = MaterialTheme.typography.bodyMedium,
)
Text(
if (columns > 0) {
stringResource(id = R.string.pref_library_columns_per_row, columns)
} else {
stringResource(id = R.string.label_default)
},
)
}
Slider(
value = columns.toFloat(),
onValueChange = { columnPreference.set(it.toInt()) },
modifier = Modifier.weight(1.5f),
valueRange = 0f..10f,
steps = 10,
)
}
val columns by columnPreference.collectAsState()
SliderItem(
label = stringResource(R.string.pref_library_columns),
max = 10,
value = columns,
valueText = if (columns > 0) {
stringResource(R.string.pref_library_columns_per_row, columns)
} else {
stringResource(R.string.label_default)
},
onChange = { columnPreference.set(it) },
)
}
HeadingItem(R.string.overlay_header)

View file

@ -11,6 +11,7 @@ import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.structuralEqualityPolicy
import androidx.compose.ui.unit.dp
import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.more.settings.widget.AppThemePreferenceWidget
@ -24,10 +25,12 @@ import eu.kanade.presentation.more.settings.widget.TrackingPreferenceWidget
import eu.kanade.presentation.util.collectAsState
import kotlinx.coroutines.launch
import tachiyomi.core.preference.PreferenceStore
import tachiyomi.presentation.core.components.SliderItem
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
val LocalPreferenceHighlighted = compositionLocalOf(structuralEqualityPolicy()) { false }
val LocalPreferenceMinHeight = compositionLocalOf(structuralEqualityPolicy()) { 56.dp }
@Composable
fun StatusWrapper(
@ -77,6 +80,21 @@ internal fun PreferenceItem(
},
)
}
is Preference.PreferenceItem.SliderPreference -> {
// TODO: use different composable?
SliderItem(
label = item.title,
min = item.min,
max = item.max,
value = item.value,
valueText = item.value.toString(),
onChange = {
scope.launch {
item.onValueChanged(it)
}
},
)
}
is Preference.PreferenceItem.ListPreference<*> -> {
val value by item.pref.collectAsState()
ListPreferenceWidget(

View file

@ -43,6 +43,20 @@ sealed class Preference {
override val onValueChanged: suspend (newValue: Boolean) -> Boolean = { true },
) : PreferenceItem<Boolean>()
/**
* A [PreferenceItem] that provides a slider to select an integer number.
*/
data class SliderPreference(
val value: Int,
val min: Int = 0,
val max: Int,
override val title: String = "",
override val subtitle: String? = null,
override val icon: ImageVector? = null,
override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: Int) -> Boolean = { true },
) : PreferenceItem<Int>()
/**
* A [PreferenceItem] that displays a list of entries as a dialog.
*/

View file

@ -1,14 +1,21 @@
package eu.kanade.presentation.more.settings.screen
import android.content.Context
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Public
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
@ -60,6 +67,7 @@ object AboutScreen : Screen() {
val uriHandler = LocalUriHandler.current
val handleBack = LocalBackPress.current
val navigator = LocalNavigator.currentOrThrow
var isCheckingUpdates by remember { mutableStateOf(false) }
Scaffold(
topBar = { scrollBehavior ->
@ -92,22 +100,41 @@ object AboutScreen : Screen() {
item {
TextPreferenceWidget(
title = stringResource(R.string.check_for_updates),
widget = {
AnimatedVisibility(visible = isCheckingUpdates) {
CircularProgressIndicator(
modifier = Modifier.size(28.dp),
strokeWidth = 3.dp,
)
}
},
onPreferenceClick = {
scope.launch {
checkVersion(context) { result ->
val updateScreen = NewUpdateScreen(
versionName = result.release.version,
changelogInfo = result.release.info,
releaseLink = result.release.releaseLink,
downloadLink = result.release.getDownloadLink(),
if (!isCheckingUpdates) {
scope.launch {
isCheckingUpdates = true
checkVersion(
context = context,
onAvailableUpdate = { result ->
val updateScreen = NewUpdateScreen(
versionName = result.release.version,
changelogInfo = result.release.info,
releaseLink = result.release.releaseLink,
downloadLink = result.release.getDownloadLink(),
)
navigator.push(updateScreen)
},
onFinish = {
isCheckingUpdates = false
},
)
navigator.push(updateScreen)
}
}
},
)
}
}
if (!BuildConfig.DEBUG) {
item {
TextPreferenceWidget(
@ -127,7 +154,7 @@ object AboutScreen : Screen() {
item {
TextPreferenceWidget(
title = stringResource(R.string.licenses),
onPreferenceClick = { navigator.push(LicensesScreen()) },
onPreferenceClick = { navigator.push(OpenSourceLicensesScreen()) },
)
}
@ -174,10 +201,13 @@ object AboutScreen : Screen() {
/**
* Checks version and shows a user prompt if an update is available.
*/
private suspend fun checkVersion(context: Context, onAvailableUpdate: (GetApplicationRelease.Result.NewUpdate) -> Unit) {
private suspend fun checkVersion(
context: Context,
onAvailableUpdate: (GetApplicationRelease.Result.NewUpdate) -> Unit,
onFinish: () -> Unit,
) {
val updateChecker = AppUpdateChecker()
withUIContext {
context.toast(R.string.update_check_look_for_updates)
try {
when (val result = withIOContext { updateChecker.checkForUpdate(context, forceCheck = true) }) {
is GetApplicationRelease.Result.NewUpdate -> {
@ -191,6 +221,8 @@ object AboutScreen : Screen() {
} catch (e: Exception) {
context.toast(e.message)
logcat(LogPriority.ERROR, e)
} finally {
onFinish()
}
}
}

View file

@ -0,0 +1,94 @@
package eu.kanade.presentation.more.settings.screen
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Public
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.text.HtmlCompat
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import com.google.android.material.textview.MaterialTextView
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.components.material.Scaffold
class OpenSourceLibraryLicenseScreen(
private val name: String,
private val website: String?,
private val license: String,
) : Screen() {
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val uriHandler = LocalUriHandler.current
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
text = name,
maxLines = 1,
)
},
navigationIcon = {
IconButton(onClick = navigator::pop) {
Icon(imageVector = Icons.Default.ArrowBack, contentDescription = null)
}
},
actions = {
if (!website.isNullOrEmpty()) {
AppBarActions(
listOf(
AppBar.Action(
title = stringResource(R.string.website),
icon = Icons.Default.Public,
onClick = { uriHandler.openUri(website) },
),
),
)
}
},
scrollBehavior = it,
)
},
) { contentPadding ->
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(contentPadding)
.padding(16.dp),
) {
HtmlLicenseText(html = license)
}
}
}
@Composable
private fun HtmlLicenseText(html: String) {
AndroidView(
factory = {
MaterialTextView(it)
},
update = {
it.text = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT)
},
)
}
}

View file

@ -9,12 +9,13 @@ import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import com.mikepenz.aboutlibraries.ui.compose.LibrariesContainer
import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults
import com.mikepenz.aboutlibraries.ui.compose.util.htmlReadyLicenseContent
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.components.material.Scaffold
class LicensesScreen : Screen() {
class OpenSourceLicensesScreen : Screen() {
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
@ -37,6 +38,14 @@ class LicensesScreen : Screen() {
badgeBackgroundColor = MaterialTheme.colorScheme.primary,
badgeContentColor = MaterialTheme.colorScheme.onPrimary,
),
onLibraryClick = {
val libraryLicenseScreen = OpenSourceLibraryLicenseScreen(
name = it.name,
website = it.website,
license = it.licenses.firstOrNull()?.htmlReadyLicenseContent.orEmpty(),
)
navigator.push(libraryLicenseScreen)
},
)
}
}

View file

@ -31,6 +31,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import eu.kanade.presentation.more.settings.LocalPreferenceHighlighted
import eu.kanade.presentation.more.settings.LocalPreferenceMinHeight
import kotlinx.coroutines.delay
import kotlin.time.Duration.Companion.seconds
@ -44,10 +45,11 @@ internal fun BasePreferenceWidget(
widget: @Composable (() -> Unit)? = null,
) {
val highlighted = LocalPreferenceHighlighted.current
val minHeight = LocalPreferenceMinHeight.current
Row(
modifier = modifier
.highlightBackground(highlighted)
.sizeIn(minHeight = 56.dp)
.sizeIn(minHeight = minHeight)
.clickable(enabled = onClick != null, onClick = { onClick?.invoke() })
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,

View file

@ -29,8 +29,6 @@ import eu.kanade.domain.ui.UiPreferences
import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode
import eu.kanade.tachiyomi.crash.CrashActivity
import eu.kanade.tachiyomi.crash.GlobalExceptionHandler
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.cache.EpisodeCache
import eu.kanade.tachiyomi.data.coil.AnimeCoverFetcher
import eu.kanade.tachiyomi.data.coil.AnimeCoverKeyer
import eu.kanade.tachiyomi.data.coil.AnimeKeyer
@ -60,7 +58,6 @@ import org.acra.ktx.initAcra
import org.acra.sender.HttpSender
import org.conscrypt.Conscrypt
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.presentation.widget.entries.anime.TachiyomiAnimeWidgetManager
import tachiyomi.presentation.widget.entries.manga.TachiyomiMangaWidgetManager
import uy.kohesive.injekt.Injekt
@ -71,12 +68,9 @@ import java.security.Security
class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
private val basePreferences: BasePreferences by injectLazy()
private val libraryPreferences: LibraryPreferences by injectLazy()
private val networkPreferences: NetworkPreferences by injectLazy()
private val disableIncognitoReceiver = DisableIncognitoReceiver()
private val chapterCache: ChapterCache by injectLazy()
private val episodeCache: EpisodeCache by injectLazy()
@SuppressLint("LaunchActivityFromNotification")
override fun onCreate() {
@ -192,11 +186,6 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
override fun onStop(owner: LifecycleOwner) {
SecureActivityDelegate.onApplicationStopped()
if (libraryPreferences.autoClearItemCache().get()) {
chapterCache.clear()
episodeCache.clear()
}
}
override fun getPackageName(): String {

View file

@ -45,7 +45,6 @@ import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
import kotlinx.serialization.json.Json
import nl.adaptivity.xmlutil.XmlDeclMode
import nl.adaptivity.xmlutil.core.XmlVersion
import nl.adaptivity.xmlutil.serialization.UnknownChildHandler
import nl.adaptivity.xmlutil.serialization.XML
import tachiyomi.core.preference.PreferenceStore
import tachiyomi.core.provider.AndroidBackupFolderProvider
@ -167,10 +166,12 @@ class AppModule(val app: Application) : InjektModule {
}
addSingletonFactory {
XML {
unknownChildHandler = UnknownChildHandler { _, _, _, _, _ -> emptyList() }
defaultPolicy {
ignoreUnknownChildren()
}
autoPolymorphic = true
xmlDeclMode = XmlDeclMode.Charset
indent = 4
indent = 2
xmlVersion = XmlVersion.XML10
}
}

View file

@ -273,13 +273,13 @@ class AnimeDownloadManager(
}
removeFromDownloadQueue(filteredEpisodes)
val (animeDir, episodeDirs) = provider.findEpisodeDirs(
filteredEpisodes,
anime,
source,
)
episodeDirs.forEach { it.delete() }
cache.removeEpisodes(filteredEpisodes, anime)
val (animeDir, episodeDirs) = provider.findEpisodeDirs(
filteredEpisodes,
anime,
source,
)
episodeDirs.forEach { it.delete() }
cache.removeEpisodes(filteredEpisodes, anime)
// Delete anime directory if empty
if (animeDir?.listFiles()?.isEmpty() == true) {

View file

@ -111,7 +111,7 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
val preferences = Injekt.get<LibraryPreferences>()
val restrictions = preferences.libraryUpdateDeviceRestriction().get()
if ((DEVICE_ONLY_ON_WIFI in restrictions) && !context.isConnectedToWifi()) {
return Result.failure()
return Result.retry()
}
// Find a running manual worker. If exists, try again later

View file

@ -169,7 +169,7 @@ internal object AnimeExtensionLoader {
}
.flatMap {
try {
when (val obj = Class.forName(it, false, classLoader).newInstance()) {
when (val obj = Class.forName(it, false, classLoader).getDeclaredConstructor().newInstance()) {
is AnimeSource -> listOf(obj)
is AnimeSourceFactory -> obj.createSources()
else -> throw Exception("Unknown source class type! ${obj.javaClass}")

View file

@ -169,7 +169,7 @@ internal object MangaExtensionLoader {
}
.flatMap {
try {
when (val obj = Class.forName(it, false, classLoader).newInstance()) {
when (val obj = Class.forName(it, false, classLoader).getDeclaredConstructor().newInstance()) {
is MangaSource -> listOf(obj)
is SourceFactory -> obj.createSources()
else -> throw Exception("Unknown source class type! ${obj.javaClass}")

View file

@ -152,6 +152,7 @@ class SourcePreferencesFragment : PreferenceFragmentCompat() {
source.setupPreferenceScreen(sourceScreen)
sourceScreen.forEach { pref ->
pref.isIconSpaceReserved = false
pref.isSingleLineTitle = false
if (pref is DialogPreference) {
pref.dialogTitle = pref.title
}

View file

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.browse.anime.migration
import eu.kanade.domain.entries.anime.model.hasCustomCover
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.AnimeCoverCache
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadCache
import kotlinx.coroutines.runBlocking
import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.domain.track.anime.interactor.GetAnimeTracks
@ -12,15 +13,18 @@ import uy.kohesive.injekt.injectLazy
object AnimeMigrationFlags {
private const val EPISODES = 0b0001
private const val CATEGORIES = 0b0010
private const val TRACK = 0b0100
private const val CUSTOM_COVER = 0b1000
private const val EPISODES = 0b00001
private const val CATEGORIES = 0b00010
private const val TRACK = 0b00100
private const val CUSTOM_COVER = 0b01000
private const val DELETE_DOWNLOADED = 0b10000
private val coverCache: AnimeCoverCache by injectLazy()
private val getTracks: GetAnimeTracks = Injekt.get()
private val downloadCache: AnimeDownloadCache by injectLazy()
val flags get() = arrayOf(EPISODES, CATEGORIES, TRACK, CUSTOM_COVER)
val flags get() = arrayOf(EPISODES, CATEGORIES, TRACK, CUSTOM_COVER, DELETE_DOWNLOADED)
private var enableFlags = emptyList<Int>().toMutableList()
fun hasEpisodes(value: Int): Boolean {
return value and EPISODES != 0
@ -38,23 +42,37 @@ object AnimeMigrationFlags {
return value and CUSTOM_COVER != 0
}
fun hasDeleteDownloaded(value: Int): Boolean {
return value and DELETE_DOWNLOADED != 0
}
fun getEnabledFlagsPositions(value: Int): List<Int> {
return flags.mapIndexedNotNull { index, flag -> if (value and flag != 0) index else null }
}
fun getFlagsFromPositions(positions: Array<Int>): Int {
return positions.fold(0) { accumulated, position -> accumulated or (1 shl position) }
val fold = positions.fold(0) { accumulated, position -> accumulated or enableFlags[position] }
enableFlags.clear()
return fold
}
fun titles(anime: Anime?): Array<Int> {
enableFlags.add(EPISODES)
enableFlags.add(CATEGORIES)
val titles = arrayOf(R.string.episodes, R.string.anime_categories).toMutableList()
if (anime != null) {
if (runBlocking { getTracks.await(anime.id) }.isNotEmpty()) {
titles.add(R.string.track)
enableFlags.add(TRACK)
}
if (anime.hasCustomCover(coverCache)) {
titles.add(R.string.custom_cover)
enableFlags.add(CUSTOM_COVER)
}
if (downloadCache.getDownloadCount(anime) > 0) {
titles.add(R.string.delete_downloaded)
enableFlags.add(DELETE_DOWNLOADED)
}
}
return titles.toTypedArray()

View file

@ -36,10 +36,10 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.animesource.AnimeSource
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.data.cache.AnimeCoverCache
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadManager
import eu.kanade.tachiyomi.data.track.EnhancedAnimeTrackService
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.ui.browse.anime.migration.AnimeMigrationFlags
import eu.kanade.tachiyomi.ui.browse.manga.migration.MangaMigrationFlags
import kotlinx.coroutines.flow.update
import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore
@ -145,7 +145,7 @@ internal fun MigrateAnimeDialog(
val selectedIndices = mutableListOf<Int>()
selected.fastForEachIndexed { i, b -> if (b) selectedIndices.add(i) }
val newValue =
MangaMigrationFlags.getFlagsFromPositions(selectedIndices.toTypedArray())
AnimeMigrationFlags.getFlagsFromPositions(selectedIndices.toTypedArray())
screenModel.migrateFlags.set(newValue)
screenModel.migrateAnime(oldAnime, newAnime, true)
withUIContext { onPopScreen() }
@ -162,6 +162,7 @@ internal fun MigrateAnimeDialog(
internal class MigrateAnimeDialogScreenModel(
private val sourceManager: AnimeSourceManager = Injekt.get(),
private val downloadManager: AnimeDownloadManager = Injekt.get(),
private val updateAnime: UpdateAnime = Injekt.get(),
private val getEpisodeByAnimeId: GetEpisodeByAnimeId = Injekt.get(),
private val syncEpisodesWithSource: SyncEpisodesWithSource = Injekt.get(),
@ -220,6 +221,7 @@ internal class MigrateAnimeDialogScreenModel(
val migrateCategories = AnimeMigrationFlags.hasCategories(flags)
val migrateTracks = AnimeMigrationFlags.hasTracks(flags)
val migrateCustomCover = AnimeMigrationFlags.hasCustomCover(flags)
val deleteDownloaded = AnimeMigrationFlags.hasDeleteDownloaded(flags)
try {
syncEpisodesWithSource.await(sourceEpisodes, newAnime, newSource)
@ -284,6 +286,13 @@ internal class MigrateAnimeDialogScreenModel(
insertTrack.awaitAll(tracks)
}
// Delete downloaded
if (deleteDownloaded) {
if (oldSource != null) {
downloadManager.deleteAnime(oldAnime, oldSource)
}
}
if (replace) {
updateAnime.await(AnimeUpdate(oldAnime.id, favorite = false, dateAdded = 0))
}

View file

@ -152,7 +152,7 @@ class BrowseAnimeSourceScreenModel(
}
fun setListing(listing: Listing) {
mutableState.update { it.copy(listing = listing) }
mutableState.update { it.copy(listing = listing, toolbarQuery = null) }
}
fun setFilters(filters: AnimeFilterList) {

View file

@ -152,6 +152,7 @@ class MangaSourcePreferencesFragment : PreferenceFragmentCompat() {
source.setupPreferenceScreen(sourceScreen)
sourceScreen.forEach { pref ->
pref.isIconSpaceReserved = false
pref.isSingleLineTitle = false
if (pref is DialogPreference) {
pref.dialogTitle = pref.title
}

View file

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.browse.manga.migration
import eu.kanade.domain.entries.manga.model.hasCustomCover
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.MangaCoverCache
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadCache
import kotlinx.coroutines.runBlocking
import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.track.manga.interactor.GetMangaTracks
@ -12,15 +13,18 @@ import uy.kohesive.injekt.injectLazy
object MangaMigrationFlags {
private const val CHAPTERS = 0b0001
private const val CATEGORIES = 0b0010
private const val TRACK = 0b0100
private const val CUSTOM_COVER = 0b1000
private const val CHAPTERS = 0b00001
private const val CATEGORIES = 0b00010
private const val TRACK = 0b00100
private const val CUSTOM_COVER = 0b01000
private const val DELETE_DOWNLOADED = 0b10000
private val coverCache: MangaCoverCache by injectLazy()
private val getTracks: GetMangaTracks = Injekt.get()
private val downloadCache: MangaDownloadCache by injectLazy()
val flags get() = arrayOf(CHAPTERS, CATEGORIES, TRACK, CUSTOM_COVER)
val flags get() = arrayOf(CHAPTERS, CATEGORIES, TRACK, CUSTOM_COVER, DELETE_DOWNLOADED)
private var enableFlags = emptyList<Int>().toMutableList()
fun hasChapters(value: Int): Boolean {
return value and CHAPTERS != 0
@ -38,23 +42,36 @@ object MangaMigrationFlags {
return value and CUSTOM_COVER != 0
}
fun hasDeleteDownloaded(value: Int): Boolean {
return value and DELETE_DOWNLOADED != 0
}
fun getEnabledFlagsPositions(value: Int): List<Int> {
return flags.mapIndexedNotNull { index, flag -> if (value and flag != 0) index else null }
}
fun getFlagsFromPositions(positions: Array<Int>): Int {
return positions.fold(0) { accumulated, position -> accumulated or (1 shl position) }
val fold = positions.fold(0) { accumulated, position -> accumulated or enableFlags[position] }
enableFlags.clear()
return fold
}
fun titles(manga: Manga?): Array<Int> {
enableFlags.add(CHAPTERS)
enableFlags.add(CATEGORIES)
val titles = arrayOf(R.string.chapters, R.string.manga_categories).toMutableList()
if (manga != null) {
if (runBlocking { getTracks.await(manga.id) }.isNotEmpty()) {
titles.add(R.string.track)
enableFlags.add(TRACK)
}
if (manga.hasCustomCover(coverCache)) {
titles.add(R.string.custom_cover)
enableFlags.add(CUSTOM_COVER)
}
if (downloadCache.getDownloadCount(manga) > 0) {
titles.add(R.string.delete_downloaded)
enableFlags.add(DELETE_DOWNLOADED)
}
}
return titles.toTypedArray()

View file

@ -34,6 +34,7 @@ import eu.kanade.domain.entries.manga.model.toSManga
import eu.kanade.domain.items.chapter.interactor.SyncChaptersWithSource
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.MangaCoverCache
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadManager
import eu.kanade.tachiyomi.data.track.EnhancedMangaTrackService
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.MangaSource
@ -161,6 +162,7 @@ internal fun MigrateMangaDialog(
internal class MigrateMangaDialogScreenModel(
private val sourceManager: MangaSourceManager = Injekt.get(),
private val downloadManager: MangaDownloadManager = Injekt.get(),
private val updateManga: UpdateManga = Injekt.get(),
private val getChapterByMangaId: GetChapterByMangaId = Injekt.get(),
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(),
@ -219,6 +221,7 @@ internal class MigrateMangaDialogScreenModel(
val migrateCategories = MangaMigrationFlags.hasCategories(flags)
val migrateTracks = MangaMigrationFlags.hasTracks(flags)
val migrateCustomCover = MangaMigrationFlags.hasCustomCover(flags)
val deleteDownloaded = MangaMigrationFlags.hasDeleteDownloaded(flags)
try {
syncChaptersWithSource.await(sourceChapters, newManga, newSource)
@ -283,6 +286,13 @@ internal class MigrateMangaDialogScreenModel(
insertTrack.awaitAll(tracks)
}
// Delete downloaded
if (deleteDownloaded) {
if (oldSource != null) {
downloadManager.deleteManga(oldManga, oldSource)
}
}
if (replace) {
updateManga.await(MangaUpdate(oldManga.id, favorite = false, dateAdded = 0))
}

View file

@ -152,7 +152,7 @@ class BrowseMangaSourceScreenModel(
}
fun setListing(listing: Listing) {
mutableState.update { it.copy(listing = listing) }
mutableState.update { it.copy(listing = listing, toolbarQuery = null) }
}
fun setFilters(filters: FilterList) {

View file

@ -123,7 +123,7 @@ class AnimeInfoScreenModel(
private val isFavorited: Boolean
get() = anime?.favorite ?: false
private val processedEpisodes: Sequence<EpisodeItem>?
private val processedEpisodes: List<EpisodeItem>?
get() = successState?.processedEpisodes
val episodeSwipeEndAction = libraryPreferences.swipeEpisodeEndAction().get()
@ -1027,8 +1027,9 @@ sealed class AnimeScreenState {
val nextAiringEpisode: Pair<Int, Long> = Pair(anime.nextEpisodeToAir, anime.nextEpisodeAiringAt),
) : AnimeScreenState() {
val processedEpisodes: Sequence<EpisodeItem>
get() = episodes.applyFilters(anime)
val processedEpisodes by lazy {
episodes.applyFilters(anime).toList()
}
val trackingAvailable: Boolean
get() = trackItems.isNotEmpty()

View file

@ -121,7 +121,7 @@ class MangaInfoScreenModel(
private val allChapters: List<ChapterItem>?
get() = successState?.chapters
private val filteredChapters: Sequence<ChapterItem>?
private val filteredChapters: List<ChapterItem>?
get() = successState?.processedChapters
val chapterSwipeEndAction = libraryPreferences.swipeChapterEndAction().get()
@ -589,7 +589,7 @@ class MangaInfoScreenModel(
}
private fun getUnreadChapters(): List<Chapter> {
val chapterItems = if (skipFiltered) filteredChapters.orEmpty().toList() else allChapters.orEmpty()
val chapterItems = if (skipFiltered) filteredChapters.orEmpty() else allChapters.orEmpty()
return chapterItems
.filter { (chapter, dlStatus) -> !chapter.read && dlStatus == MangaDownload.State.NOT_DOWNLOADED }
.map { it.chapter }
@ -677,7 +677,7 @@ class MangaInfoScreenModel(
fun markPreviousChapterRead(pointer: Chapter) {
val successState = successState ?: return
val chapters = filteredChapters.orEmpty().map { it.chapter }.toList()
val chapters = filteredChapters.orEmpty().map { it.chapter }
val prevChapters = if (successState.manga.sortDescending()) chapters.asReversed() else chapters
val pointerPos = prevChapters.indexOf(pointer)
if (pointerPos != -1) markChaptersRead(prevChapters.take(pointerPos), true)
@ -1000,8 +1000,9 @@ sealed class MangaScreenState {
val hasPromptedToAddBefore: Boolean = false,
) : MangaScreenState() {
val processedChapters: Sequence<ChapterItem>
get() = chapters.applyFilters(manga)
val processedChapters by lazy {
chapters.applyFilters(manga).toList()
}
val trackingAvailable: Boolean
get() = trackItems.isNotEmpty()

View file

@ -70,6 +70,8 @@ import eu.kanade.tachiyomi.Migrations
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.core.Constants
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.cache.EpisodeCache
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadCache
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadCache
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
@ -121,6 +123,8 @@ class MainActivity : BaseActivity() {
private val animeDownloadCache: AnimeDownloadCache by injectLazy()
private val downloadCache: MangaDownloadCache by injectLazy()
private val chapterCache: ChapterCache by injectLazy()
private val episodeCache: EpisodeCache by injectLazy()
// To be checked by splash screen. If true then splash screen will be removed.
var ready = false
@ -128,12 +132,14 @@ class MainActivity : BaseActivity() {
private var navigator: Navigator? = null
override fun onCreate(savedInstanceState: Bundle?) {
val isLaunch = savedInstanceState == null
// Prevent splash screen showing up on configuration changes
val splashScreen = if (savedInstanceState == null) installSplashScreen() else null
val splashScreen = if (isLaunch) installSplashScreen() else null
super.onCreate(savedInstanceState)
val didMigration = if (savedInstanceState == null) {
val didMigration = if (isLaunch) {
Migrations.upgrade(
context = applicationContext,
basePreferences = preferences,
@ -167,7 +173,7 @@ class MainActivity : BaseActivity() {
val indexing by downloadCache.isInitializing.collectAsState()
val indexingAnime by animeDownloadCache.isInitializing.collectAsState()
// Set statusbar color considering the top app state banner
// Set status bar color considering the top app state banner
val systemUiController = rememberSystemUiController()
val isSystemInDarkTheme = isSystemInDarkTheme()
val statusBarBackgroundColor = when {
@ -208,7 +214,7 @@ class MainActivity : BaseActivity() {
LaunchedEffect(navigator) {
this@MainActivity.navigator = navigator
if (savedInstanceState == null) {
if (isLaunch) {
// Set start screen
handleIntentAction(intent, navigator)
@ -287,6 +293,11 @@ class MainActivity : BaseActivity() {
}
setSplashScreenExitAnimation(splashScreen)
if (isLaunch && libraryPreferences.autoClearItemCache().get()) {
chapterCache.clear()
episodeCache.clear()
}
externalPlayerResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK) {
ExternalIntents.externalIntents.onActivityResult(result.data)
@ -304,7 +315,7 @@ class MainActivity : BaseActivity() {
}
@Composable
fun HandleOnNewIntent(context: Context, navigator: Navigator) {
private fun HandleOnNewIntent(context: Context, navigator: Navigator) {
LaunchedEffect(Unit) {
callbackFlow<Intent> {
val componentActivity = context as ComponentActivity

View file

@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.reader
import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.app.ProgressDialog
import android.app.assist.AssistContent
import android.content.Context
import android.content.Intent
@ -25,8 +24,15 @@ import android.view.animation.Animation
import android.view.animation.AnimationUtils
import android.widget.Toast
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.dp
import androidx.core.graphics.ColorUtils
import androidx.core.net.toUri
import androidx.core.transition.doOnEnd
@ -59,6 +65,7 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReaderColorFilterDialog
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsSheet
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
@ -92,7 +99,9 @@ import tachiyomi.core.util.lang.launchNonCancellable
import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.entries.manga.model.Manga
import uy.kohesive.injekt.injectLazy
import tachiyomi.presentation.widget.util.stringResource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.math.abs
class ReaderActivity : BaseActivity() {
@ -107,8 +116,8 @@ class ReaderActivity : BaseActivity() {
}
}
private val readerPreferences: ReaderPreferences by injectLazy()
private val preferences: BasePreferences by injectLazy()
private val readerPreferences = Injekt.get<ReaderPreferences>()
private val preferences = Injekt.get<BasePreferences>()
lateinit var binding: ReaderActivityBinding
@ -117,25 +126,12 @@ class ReaderActivity : BaseActivity() {
val hasCutout by lazy { hasDisplayCutout() }
/**
* Whether the menu is currently visible.
*/
var menuVisible = false
private set
/**
* Configuration at reader level, like background color or forced orientation.
*/
private var config: ReaderConfig? = null
/**
* Progress dialog used when switching chapters from the menu buttons.
*/
@Suppress("DEPRECATION")
private var progressDialog: ProgressDialog? = null
private var menuToggleToast: Toast? = null
private var readingModeToast: Toast? = null
private val windowInsetsController by lazy { WindowInsetsControllerCompat(window, binding.root) }
@ -158,8 +154,8 @@ class ReaderActivity : BaseActivity() {
setContentView(binding.root)
if (viewModel.needsInit()) {
val manga = intent.extras!!.getLong("manga", -1)
val chapter = intent.extras!!.getLong("chapter", -1)
val manga = intent.extras?.getLong("manga", -1) ?: -1L
val chapter = intent.extras?.getLong("chapter", -1) ?: -1L
if (manga == -1L || chapter == -1L) {
finish()
return
@ -177,10 +173,6 @@ class ReaderActivity : BaseActivity() {
}
}
if (savedInstanceState != null) {
menuVisible = savedInstanceState.getBoolean(::menuVisible.name)
}
config = ReaderConfig()
initializeMenu()
@ -242,23 +234,6 @@ class ReaderActivity : BaseActivity() {
config = null
menuToggleToast?.cancel()
readingModeToast?.cancel()
progressDialog?.dismiss()
progressDialog = null
}
/**
* Called when the activity is saving instance state. Current progress is persisted if this
* activity isn't changing configurations.
*/
override fun onSaveInstanceState(outState: Bundle) {
outState.putBoolean(::menuVisible.name, menuVisible)
viewModel.onSaveInstanceState()
super.onSaveInstanceState(outState)
}
override fun onPause() {
viewModel.saveCurrentChapterReadingProgress()
super.onPause()
}
/**
@ -268,7 +243,7 @@ class ReaderActivity : BaseActivity() {
override fun onResume() {
super.onResume()
viewModel.setReadStartTime()
setMenuVisibility(menuVisible, animate = false)
setMenuVisibility(viewModel.state.value.menuVisible, animate = false)
}
/**
@ -278,7 +253,7 @@ class ReaderActivity : BaseActivity() {
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
setMenuVisibility(menuVisible, animate = false)
setMenuVisibility(viewModel.state.value.menuVisible, animate = false)
}
}
@ -413,14 +388,41 @@ class ReaderActivity : BaseActivity() {
binding.dialogRoot.setComposeContent {
val state by viewModel.state.collectAsState()
val onDismissRequest = viewModel::closeDialog
when (state.dialog) {
is ReaderViewModel.Dialog.Page -> ReaderPageDialog(
onDismissRequest = viewModel::closeDialog,
onSetAsCover = viewModel::setAsCover,
onShare = viewModel::shareImage,
onSave = viewModel::saveImage,
)
is ReaderViewModel.Dialog.Loading -> {
AlertDialog(
onDismissRequest = {},
confirmButton = {},
text = {
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
CircularProgressIndicator()
Text(stringResource(R.string.loading))
}
},
)
}
is ReaderViewModel.Dialog.ColorFilter -> {
setMenuVisibility(false)
ReaderColorFilterDialog(
onDismissRequest = {
onDismissRequest()
setMenuVisibility(true)
},
readerPreferences = viewModel.readerPreferences,
)
}
is ReaderViewModel.Dialog.PageActions -> {
ReaderPageActionsDialog(
onDismissRequest = onDismissRequest,
onSetAsCover = viewModel::setAsCover,
onShare = viewModel::shareImage,
onSave = viewModel::saveImage,
)
}
null -> {}
}
}
@ -465,7 +467,7 @@ class ReaderActivity : BaseActivity() {
}
// Set initial visibility
setMenuVisibility(menuVisible)
setMenuVisibility(viewModel.state.value.menuVisible)
}
private fun initBottomShortcuts() {
@ -552,11 +554,14 @@ class ReaderActivity : BaseActivity() {
if (readerSettingSheet?.isShowing == true) return@setOnClickListener
readerSettingSheet = ReaderSettingsSheet(this@ReaderActivity).apply { show() }
}
}
setOnLongClickListener {
if (readerSettingSheet?.isShowing == true) return@setOnLongClickListener false
readerSettingSheet = ReaderSettingsSheet(this@ReaderActivity, showColorFilterSettings = true).apply { show() }
true
// Color filter sheet
with(binding.actionColorSettings) {
setTooltip(R.string.custom_filter)
setOnClickListener {
viewModel.openColorFilterDialog()
}
}
}
@ -588,7 +593,7 @@ class ReaderActivity : BaseActivity() {
* [animate] the views.
*/
fun setMenuVisibility(visible: Boolean, animate: Boolean = true) {
menuVisible = visible
viewModel.showMenus(visible)
if (visible) {
windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
binding.readerMenu.isVisible = true
@ -747,13 +752,11 @@ class ReaderActivity : BaseActivity() {
* [show]. This is only used when the next/previous buttons on the toolbar are clicked; the
* other cases are handled with chapter transitions on the viewers and chapter preloading.
*/
@Suppress("DEPRECATION")
private fun setProgressDialog(show: Boolean) {
progressDialog?.dismiss()
progressDialog = if (show) {
ProgressDialog.show(this, null, getString(R.string.loading), true)
if (show) {
viewModel.showLoadingDialog()
} else {
null
viewModel.closeDialog()
}
}
@ -820,14 +823,14 @@ class ReaderActivity : BaseActivity() {
* viewer because each one implements its own touch and key events.
*/
fun toggleMenu() {
setMenuVisibility(!menuVisible)
setMenuVisibility(!viewModel.state.value.menuVisible)
}
/**
* Called from the viewer to show the menu.
*/
fun showMenu() {
if (!menuVisible) {
if (!viewModel.state.value.menuVisible) {
setMenuVisibility(true)
}
}
@ -836,7 +839,7 @@ class ReaderActivity : BaseActivity() {
* Called from the viewer to hide the menu.
*/
fun hideMenu() {
if (menuVisible) {
if (viewModel.state.value.menuVisible) {
setMenuVisibility(false)
}
}
@ -1034,7 +1037,7 @@ class ReaderActivity : BaseActivity() {
}
// Trigger relayout
setMenuVisibility(menuVisible)
setMenuVisibility(viewModel.state.value.menuVisible)
}
/**

View file

@ -25,7 +25,7 @@ import tachiyomi.presentation.core.components.ActionButton
import tachiyomi.presentation.core.components.material.padding
@Composable
fun ReaderPageDialog(
fun ReaderPageActionsDialog(
onDismissRequest: () -> Unit,
onSetAsCover: () -> Unit,
onShare: () -> Unit,

View file

@ -92,9 +92,9 @@ class ReaderViewModel(
private val downloadProvider: MangaDownloadProvider = Injekt.get(),
private val imageSaver: ImageSaver = Injekt.get(),
preferences: BasePreferences = Injekt.get(),
val readerPreferences: ReaderPreferences = Injekt.get(),
private val basePreferences: BasePreferences = Injekt.get(),
private val downloadPreferences: DownloadPreferences = Injekt.get(),
private val readerPreferences: ReaderPreferences = Injekt.get(),
private val trackPreferences: TrackPreferences = Injekt.get(),
private val delayedTrackingStore: DelayedMangaTrackingStore = Injekt.get(),
private val getManga: GetManga = Injekt.get(),
@ -223,7 +223,6 @@ class ReaderViewModel(
val currentChapters = state.value.viewerChapters
if (currentChapters != null) {
currentChapters.unref()
saveReadingProgress(currentChapters.currChapter)
chapterToDownload?.let {
downloadManager.addDownloadsToStartOfQueue(listOf(it))
}
@ -238,17 +237,6 @@ class ReaderViewModel(
deletePendingChapters()
}
/**
* Called when the activity is saved. It updates the database
* to persist the current progress of the active chapter.
*/
fun onSaveInstanceState() {
val currentChapter = getCurrentChapter() ?: return
viewModelScope.launchNonCancellable {
saveChapterProgress(currentChapter)
}
}
/**
* Whether this presenter is initialized yet.
*/
@ -346,7 +334,6 @@ class ReaderViewModel(
*/
private suspend fun loadAdjacent(chapter: ReaderChapter) {
val loader = loader ?: return
saveCurrentChapterReadingProgress()
logcat { "Loading adjacent ${chapter.chapter.url}" }
@ -420,16 +407,17 @@ class ReaderViewModel(
* [page]'s chapter is different from the currently active.
*/
fun onPageSelected(page: ReaderPage) {
val currentChapters = state.value.viewerChapters ?: return
val selectedChapter = page.chapter
// InsertPage and StencilPage doesn't change page progress
if (page is InsertPage || page is StencilPage) {
return
}
val currentChapters = state.value.viewerChapters ?: return
val pages = page.chapter.pages ?: return
val selectedChapter = page.chapter
// Save last page read and mark as read if needed
saveReadingProgress()
mutableState.update {
it.copy(
currentPage = page.index + 1,
@ -446,11 +434,9 @@ class ReaderViewModel(
if (selectedChapter != currentChapters.currChapter) {
logcat { "Setting ${selectedChapter.chapter.url} as active" }
saveReadingProgress(currentChapters.currChapter)
setReadStartTime()
viewModelScope.launch { loadNewChapter(selectedChapter) }
}
val pages = page.chapter.pages ?: return
val inDownloadRange = page.number.toDouble() / pages.size > 0.25
if (inDownloadRange) {
downloadNextChapters()
@ -473,7 +459,7 @@ class ReaderViewModel(
manga.title,
manga.source,
)
if (!isNextChapterDownloaded) return@launchIO
if (isNextChapterDownloaded) return@launchIO
val chaptersToDownload = getNextChapters.await(manga.id, nextChapter.id!!).run {
if (readerPreferences.skipDupe().get()) {
@ -520,17 +506,15 @@ class ReaderViewModel(
}
}
fun saveCurrentChapterReadingProgress() {
getCurrentChapter()?.let { saveReadingProgress(it) }
}
/**
* Called when reader chapter is changed in reader or when activity is paused.
*/
private fun saveReadingProgress(readerChapter: ReaderChapter) {
viewModelScope.launchNonCancellable {
saveChapterProgress(readerChapter)
saveChapterHistory(readerChapter)
private fun saveReadingProgress() {
getCurrentChapter()?.let {
viewModelScope.launchNonCancellable {
saveChapterProgress(it)
saveChapterHistory(it)
}
}
}
@ -542,7 +526,7 @@ class ReaderViewModel(
if (!incognitoMode) return
val chapter = readerChapter.chapter
getCurrentChapter()?.requestedPage = chapter.last_page_read
readerChapter.requestedPage = chapter.last_page_read
updateChapter.await(
ChapterUpdate(
id = chapter.id!!,
@ -718,8 +702,20 @@ class ReaderViewModel(
) + filenameSuffix
}
fun showMenus(visible: Boolean) {
mutableState.update { it.copy(menuVisible = visible) }
}
fun showLoadingDialog() {
mutableState.update { it.copy(dialog = Dialog.Loading) }
}
fun openPageDialog(page: ReaderPage) {
mutableState.update { it.copy(dialog = Dialog.Page(page)) }
mutableState.update { it.copy(dialog = Dialog.PageActions(page)) }
}
fun openColorFilterDialog() {
mutableState.update { it.copy(dialog = Dialog.ColorFilter) }
}
fun closeDialog() {
@ -731,7 +727,7 @@ class ReaderViewModel(
* There's also a notification to allow sharing the image somewhere else or deleting it.
*/
fun saveImage() {
val page = (state.value.dialog as? Dialog.Page)?.page
val page = (state.value.dialog as? Dialog.PageActions)?.page
if (page?.status != Page.State.READY) return
val manga = manga ?: return
@ -773,7 +769,7 @@ class ReaderViewModel(
* image will be kept so it won't be taking lots of internal disk space.
*/
fun shareImage() {
val page = (state.value.dialog as? Dialog.Page)?.page
val page = (state.value.dialog as? Dialog.PageActions)?.page
if (page?.status != Page.State.READY) return
val manga = manga ?: return
@ -803,7 +799,7 @@ class ReaderViewModel(
* Sets the image of this the selected page as cover and notifies the UI of the result.
*/
fun setAsCover() {
val page = (state.value.dialog as? Dialog.Page)?.page
val page = (state.value.dialog as? Dialog.PageActions)?.page
if (page?.status != Page.State.READY) return
val manga = manga ?: return
val stream = page.stream ?: return
@ -918,13 +914,16 @@ class ReaderViewModel(
*/
val viewer: Viewer? = null,
val dialog: Dialog? = null,
val menuVisible: Boolean = false,
) {
val totalPages: Int
get() = viewerChapters?.currChapter?.pages?.size ?: -1
}
sealed class Dialog {
data class Page(val page: ReaderPage) : Dialog()
object Loading : Dialog()
object ColorFilter : Dialog()
data class PageActions(val page: ReaderPage) : Dialog()
}
sealed class Event {

View file

@ -0,0 +1,164 @@
package eu.kanade.tachiyomi.ui.reader.setting
import android.os.Build
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogWindowProvider
import androidx.core.graphics.alpha
import androidx.core.graphics.blue
import androidx.core.graphics.green
import androidx.core.graphics.red
import eu.kanade.presentation.components.AdaptiveSheet
import eu.kanade.presentation.more.settings.LocalPreferenceMinHeight
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.PreferenceScreen
import eu.kanade.presentation.util.collectAsState
import eu.kanade.tachiyomi.R
import tachiyomi.core.preference.getAndSet
@Composable
fun ReaderColorFilterDialog(
onDismissRequest: () -> Unit,
readerPreferences: ReaderPreferences,
) {
val colorFilterModes = buildList {
addAll(
listOf(
R.string.label_default,
R.string.filter_mode_multiply,
R.string.filter_mode_screen,
),
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
addAll(
listOf(
R.string.filter_mode_overlay,
R.string.filter_mode_lighten,
R.string.filter_mode_darken,
),
)
}
}.map { stringResource(it) }
val customBrightness by readerPreferences.customBrightness().collectAsState()
val customBrightnessValue by readerPreferences.customBrightnessValue().collectAsState()
val colorFilter by readerPreferences.colorFilter().collectAsState()
val colorFilterValue by readerPreferences.colorFilterValue().collectAsState()
val colorFilterMode by readerPreferences.colorFilterMode().collectAsState()
AdaptiveSheet(
onDismissRequest = onDismissRequest,
) {
(LocalView.current.parent as? DialogWindowProvider)?.window?.setDimAmount(0f)
CompositionLocalProvider(
LocalPreferenceMinHeight provides 48.dp,
) {
PreferenceScreen(
items = listOfNotNull(
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.customBrightness(),
title = stringResource(R.string.pref_custom_brightness),
),
/**
* Sets the brightness of the screen. Range is [-75, 100].
* From -75 to -1 a semi-transparent black view is shown at the top with the minimum brightness.
* From 1 to 100 it sets that value as brightness.
* 0 sets system brightness and hides the overlay.
*/
Preference.PreferenceItem.SliderPreference(
value = customBrightnessValue,
title = stringResource(R.string.pref_custom_brightness),
min = -75,
max = 100,
onValueChanged = {
readerPreferences.customBrightnessValue().set(it)
true
},
).takeIf { customBrightness },
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.colorFilter(),
title = stringResource(R.string.pref_custom_color_filter),
),
Preference.PreferenceItem.SliderPreference(
value = colorFilterValue.red,
title = stringResource(R.string.color_filter_r_value),
max = 255,
onValueChanged = { newRValue ->
readerPreferences.colorFilterValue().getAndSet {
getColorValue(it, newRValue, RED_MASK, 16)
}
true
},
).takeIf { colorFilter },
Preference.PreferenceItem.SliderPreference(
value = colorFilterValue.green,
title = stringResource(R.string.color_filter_g_value),
max = 255,
onValueChanged = { newRValue ->
readerPreferences.colorFilterValue().getAndSet {
getColorValue(it, newRValue, GREEN_MASK, 8)
}
true
},
).takeIf { colorFilter },
Preference.PreferenceItem.SliderPreference(
value = colorFilterValue.blue,
title = stringResource(R.string.color_filter_b_value),
max = 255,
onValueChanged = { newRValue ->
readerPreferences.colorFilterValue().getAndSet {
getColorValue(it, newRValue, BLUE_MASK, 0)
}
true
},
).takeIf { colorFilter },
Preference.PreferenceItem.SliderPreference(
value = colorFilterValue.alpha,
title = stringResource(R.string.color_filter_a_value),
max = 255,
onValueChanged = { newRValue ->
readerPreferences.colorFilterValue().getAndSet {
getColorValue(it, newRValue, ALPHA_MASK, 24)
}
true
},
).takeIf { colorFilter },
Preference.PreferenceItem.BasicListPreference(
value = colorFilterMode.toString(),
title = stringResource(R.string.pref_color_filter_mode),
entries = colorFilterModes
.mapIndexed { index, mode -> index.toString() to mode }
.toMap(),
onValueChanged = { newValue ->
readerPreferences.colorFilterMode().set(newValue.toInt())
true
},
).takeIf { colorFilter },
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.grayscale(),
title = stringResource(R.string.pref_grayscale),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.invertedColors(),
title = stringResource(R.string.pref_inverted_colors),
),
),
)
}
}
}
private fun getColorValue(currentColor: Int, color: Int, mask: Long, bitShift: Int): Int {
return (color shl bitShift) or (currentColor and mask.inv().toInt())
}
private const val ALPHA_MASK: Long = 0xFF000000
private const val RED_MASK: Long = 0x00FF0000
private const val GREEN_MASK: Long = 0x0000FF00
private const val BLUE_MASK: Long = 0x000000FF

View file

@ -1,202 +0,0 @@
package eu.kanade.tachiyomi.ui.reader.setting
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import androidx.annotation.ColorInt
import androidx.core.graphics.alpha
import androidx.core.graphics.blue
import androidx.core.graphics.green
import androidx.core.graphics.red
import androidx.core.widget.NestedScrollView
import androidx.lifecycle.lifecycleScope
import eu.kanade.tachiyomi.databinding.ReaderColorFilterSettingsBinding
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.preference.bindToPreference
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.sample
import tachiyomi.core.preference.getAndSet
import uy.kohesive.injekt.injectLazy
/**
* Color filter sheet to toggle custom filter and brightness overlay.
*/
class ReaderColorFilterSettings @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
NestedScrollView(context, attrs) {
private val readerPreferences: ReaderPreferences by injectLazy()
private val binding = ReaderColorFilterSettingsBinding.inflate(LayoutInflater.from(context), this, false)
init {
addView(binding.root)
readerPreferences.colorFilter().changes()
.onEach(::setColorFilter)
.launchIn((context as ReaderActivity).lifecycleScope)
readerPreferences.colorFilterMode().changes()
.onEach { setColorFilter(readerPreferences.colorFilter().get()) }
.launchIn(context.lifecycleScope)
readerPreferences.customBrightness().changes()
.onEach(::setCustomBrightness)
.launchIn(context.lifecycleScope)
// Get color and update values
val color = readerPreferences.colorFilterValue().get()
val brightness = readerPreferences.customBrightnessValue().get()
val argb = setValues(color)
// Set brightness value
binding.txtBrightnessSeekbarValue.text = brightness.toString()
binding.sliderBrightness.value = brightness.toFloat()
// Initialize seekBar progress
binding.sliderColorFilterAlpha.value = argb[0].toFloat()
binding.sliderColorFilterRed.value = argb[1].toFloat()
binding.sliderColorFilterGreen.value = argb[2].toFloat()
binding.sliderColorFilterBlue.value = argb[3].toFloat()
// Set listeners
binding.switchColorFilter.bindToPreference(readerPreferences.colorFilter())
binding.customBrightness.bindToPreference(readerPreferences.customBrightness())
binding.colorFilterMode.bindToPreference(readerPreferences.colorFilterMode())
binding.grayscale.bindToPreference(readerPreferences.grayscale())
binding.invertedColors.bindToPreference(readerPreferences.invertedColors())
binding.sliderColorFilterAlpha.addOnChangeListener { _, value, fromUser ->
if (fromUser) {
setColorValue(value.toInt(), ALPHA_MASK, 24)
}
}
binding.sliderColorFilterRed.addOnChangeListener { _, value, fromUser ->
if (fromUser) {
setColorValue(value.toInt(), RED_MASK, 16)
}
}
binding.sliderColorFilterGreen.addOnChangeListener { _, value, fromUser ->
if (fromUser) {
setColorValue(value.toInt(), GREEN_MASK, 8)
}
}
binding.sliderColorFilterBlue.addOnChangeListener { _, value, fromUser ->
if (fromUser) {
setColorValue(value.toInt(), BLUE_MASK, 0)
}
}
binding.sliderBrightness.addOnChangeListener { _, value, fromUser ->
if (fromUser) {
readerPreferences.customBrightnessValue().set(value.toInt())
}
}
}
/**
* Set enabled status of seekBars belonging to color filter
* @param enabled determines if seekBar gets enabled
*/
private fun setColorFilterSeekBar(enabled: Boolean) {
binding.sliderColorFilterRed.isEnabled = enabled
binding.sliderColorFilterGreen.isEnabled = enabled
binding.sliderColorFilterBlue.isEnabled = enabled
binding.sliderColorFilterAlpha.isEnabled = enabled
}
/**
* Set enabled status of seekBars belonging to custom brightness
* @param enabled value which determines if seekBar gets enabled
*/
private fun setCustomBrightnessSeekBar(enabled: Boolean) {
binding.sliderBrightness.isEnabled = enabled
}
/**
* Set the text value's of color filter
* @param color integer containing color information
*/
private fun setValues(color: Int): Array<Int> {
val alpha = color.alpha
val red = color.red
val green = color.green
val blue = color.blue
// Initialize values
binding.txtColorFilterAlphaValue.text = "$alpha"
binding.txtColorFilterRedValue.text = "$red"
binding.txtColorFilterGreenValue.text = "$green"
binding.txtColorFilterBlueValue.text = "$blue"
return arrayOf(alpha, red, green, blue)
}
/**
* Manages the custom brightness value subscription
* @param enabled determines if the subscription get (un)subscribed
*/
private fun setCustomBrightness(enabled: Boolean) {
if (enabled) {
readerPreferences.customBrightnessValue().changes()
.sample(100)
.onEach(::setCustomBrightnessValue)
.launchIn((context as ReaderActivity).lifecycleScope)
} else {
setCustomBrightnessValue(0, true)
}
setCustomBrightnessSeekBar(enabled)
}
/**
* Sets the brightness of the screen. Range is [-75, 100].
* From -75 to -1 a semi-transparent black view is shown at the top with the minimum brightness.
* From 1 to 100 it sets that value as brightness.
* 0 sets system brightness and hides the overlay.
*/
private fun setCustomBrightnessValue(value: Int, isDisabled: Boolean = false) {
if (!isDisabled) {
binding.txtBrightnessSeekbarValue.text = value.toString()
}
}
/**
* Manages the color filter value subscription
* @param enabled determines if the subscription get (un)subscribed
*/
private fun setColorFilter(enabled: Boolean) {
if (enabled) {
readerPreferences.colorFilterValue().changes()
.sample(100)
.onEach(::setColorFilterValue)
.launchIn((context as ReaderActivity).lifecycleScope)
}
setColorFilterSeekBar(enabled)
}
/**
* Sets the color filter overlay of the screen. Determined by HEX of integer
* @param color hex of color.
*/
private fun setColorFilterValue(@ColorInt color: Int) {
setValues(color)
}
/**
* Updates the color value in preference
* @param color value of color range [0,255]
* @param mask contains hex mask of chosen color
* @param bitShift amounts of bits that gets shifted to receive value
*/
private fun setColorValue(color: Int, mask: Long, bitShift: Int) {
readerPreferences.colorFilterValue().getAndSet { currentColor ->
(color shl bitShift) or (currentColor and mask.inv().toInt())
}
}
}
private const val ALPHA_MASK: Long = 0xFF000000
private const val RED_MASK: Long = 0x00FF0000
private const val GREEN_MASK: Long = 0x0000FF00
private const val BLUE_MASK: Long = 0x000000FF

View file

@ -1,46 +1,30 @@
package eu.kanade.tachiyomi.ui.reader.setting
import android.animation.ValueAnimator
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.google.android.material.tabs.TabLayout
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.CommonTabbedSheetBinding
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.widget.ViewPagerAdapter
import eu.kanade.tachiyomi.widget.listener.SimpleTabSelectedListener
import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog
class ReaderSettingsSheet(
private val activity: ReaderActivity,
private val showColorFilterSettings: Boolean = false,
) : BaseBottomSheetDialog(activity) {
private val tabs = listOf(
ReaderReadingModeSettings(activity) to R.string.pref_category_reading_mode,
ReaderGeneralSettings(activity) to R.string.pref_category_general,
ReaderColorFilterSettings(activity) to R.string.custom_filter,
)
private val backgroundDimAnimator by lazy {
val sheetBackgroundDim = window?.attributes?.dimAmount ?: 0.25f
ValueAnimator.ofFloat(sheetBackgroundDim, 0f).also { valueAnimator ->
valueAnimator.duration = 250
valueAnimator.addUpdateListener {
window?.setDimAmount(it.animatedValue as Float)
}
}
}
private lateinit var binding: CommonTabbedSheetBinding
override fun createView(inflater: LayoutInflater): View {
binding = CommonTabbedSheetBinding.inflate(activity.layoutInflater)
val adapter = Adapter()
binding.pager.offscreenPageLimit = 2
binding.pager.adapter = adapter
binding.tabs.setupWithViewPager(binding.pager)
@ -52,35 +36,6 @@ class ReaderSettingsSheet(
behavior.isFitToContents = false
behavior.halfExpandedRatio = 0.25f
val filterTabIndex = tabs.indexOfFirst { it.first is ReaderColorFilterSettings }
binding.tabs.addOnTabSelectedListener(
object : SimpleTabSelectedListener() {
override fun onTabSelected(tab: TabLayout.Tab?) {
val isFilterTab = tab?.position == filterTabIndex
// Remove dimmed backdrop so color filter changes can be previewed
backgroundDimAnimator.run {
if (isFilterTab) {
if (animatedFraction < 1f) {
start()
}
} else if (animatedFraction > 0f) {
reverse()
}
}
// Hide toolbars
if (activity.menuVisible != !isFilterTab) {
activity.setMenuVisibility(!isFilterTab)
}
}
},
)
if (showColorFilterSettings) {
binding.tabs.getTabAt(filterTabIndex)?.select()
}
}
private inner class Adapter : ViewPagerAdapter() {

View file

@ -112,7 +112,7 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer {
}
}
pager.longTapListener = f@{
if (activity.menuVisible || config.longTapEnabled) {
if (activity.viewModel.state.value.menuVisible || config.longTapEnabled) {
val item = adapter.items.getOrNull(pager.currentItem)
if (item is ReaderPage) {
activity.onPageLongTap(item)
@ -374,14 +374,14 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer {
when (event.keyCode) {
KeyEvent.KEYCODE_VOLUME_DOWN -> {
if (!config.volumeKeysEnabled || activity.menuVisible) {
if (!config.volumeKeysEnabled || activity.viewModel.state.value.menuVisible) {
return false
} else if (isUp) {
if (!config.volumeKeysInverted) moveDown() else moveUp()
}
}
KeyEvent.KEYCODE_VOLUME_UP -> {
if (!config.volumeKeysEnabled || activity.menuVisible) {
if (!config.volumeKeysEnabled || activity.viewModel.state.value.menuVisible) {
return false
} else if (isUp) {
if (!config.volumeKeysInverted) moveUp() else moveDown()

View file

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
import android.content.Context
import android.graphics.Rect
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.ScaleGestureDetector
@ -44,6 +45,22 @@ class WebtoonFrame(context: Context) : FrameLayout(context) {
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
scaleDetector.onTouchEvent(ev)
flingDetector.onTouchEvent(ev)
// Get the bounding box of the recyclerview and translate any motion events to be within it.
// Used to allow scrolling outside the recyclerview.
val recyclerRect = Rect()
recycler?.getHitRect(recyclerRect) ?: return super.dispatchTouchEvent(ev)
// Shrink the box to account for any rounding issues.
recyclerRect.inset(1, 1)
if (recyclerRect.right < recyclerRect.left || recyclerRect.bottom < recyclerRect.top) {
return super.dispatchTouchEvent(ev)
}
ev.setLocation(
ev.x.coerceIn(recyclerRect.left.toFloat(), recyclerRect.right.toFloat()),
ev.y.coerceIn(recyclerRect.top.toFloat(), recyclerRect.bottom.toFloat()),
)
return super.dispatchTouchEvent(ev)
}

View file

@ -91,7 +91,7 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
onScrolled()
if ((dy > threshold || dy < -threshold) && activity.menuVisible) {
if ((dy > threshold || dy < -threshold) && activity.viewModel.state.value.menuVisible) {
activity.hideMenu()
}
@ -120,7 +120,7 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
}
}
recycler.longTapListener = f@{ event ->
if (activity.menuVisible || config.longTapEnabled) {
if (activity.viewModel.state.value.menuVisible || config.longTapEnabled) {
val child = recycler.findChildViewUnder(event.x, event.y)
if (child != null) {
val position = recycler.getChildAdapterPosition(child)
@ -310,14 +310,14 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
when (event.keyCode) {
KeyEvent.KEYCODE_VOLUME_DOWN -> {
if (!config.volumeKeysEnabled || activity.menuVisible) {
if (!config.volumeKeysEnabled || activity.viewModel.state.value.menuVisible) {
return false
} else if (isUp) {
if (!config.volumeKeysInverted) scrollDown() else scrollUp()
}
}
KeyEvent.KEYCODE_VOLUME_UP -> {
if (!config.volumeKeysEnabled || activity.menuVisible) {
if (!config.volumeKeysEnabled || activity.viewModel.state.value.menuVisible) {
return false
} else if (isUp) {
if (!config.volumeKeysInverted) scrollUp() else scrollDown()

View file

@ -1,21 +0,0 @@
package eu.kanade.tachiyomi.util.view
import android.widget.ImageView
import androidx.annotation.AttrRes
import androidx.annotation.DrawableRes
import androidx.appcompat.content.res.AppCompatResources
import eu.kanade.tachiyomi.util.system.getResourceColor
/**
* Set a vector on a [ImageView].
*
* @param drawable id of drawable resource
*/
fun ImageView.setVectorCompat(@DrawableRes drawable: Int, @AttrRes tint: Int? = null) {
val vector = AppCompatResources.getDrawable(context, drawable)
if (tint != null) {
vector?.mutate()
vector?.setTint(context.getResourceColor(tint))
}
setImageDrawable(vector)
}

View file

@ -1,15 +0,0 @@
package eu.kanade.tachiyomi.util.view
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.LayoutRes
/**
* Extension method to inflate a view directly from its parent.
* @param layout the layout to inflate.
* @param attachToRoot whether to attach the view to the root or not. Defaults to false.
*/
fun ViewGroup.inflate(@LayoutRes layout: Int, attachToRoot: Boolean = false): View {
return LayoutInflater.from(context).inflate(layout, this, attachToRoot)
}

View file

@ -1,56 +0,0 @@
package eu.kanade.tachiyomi.widget
import android.graphics.Canvas
import android.graphics.Paint
import android.text.style.ReplacementSpan
import androidx.annotation.ColorInt
import androidx.annotation.Dimension
/**
* Source: https://github.com/santaevpavel
*
* A class that draws the outlines of a text when given a stroke color and stroke width.
*/
class OutlineSpan(
@ColorInt private val strokeColor: Int,
@Dimension private val strokeWidth: Float,
) : ReplacementSpan() {
override fun getSize(
paint: Paint,
text: CharSequence,
start: Int,
end: Int,
fm: Paint.FontMetricsInt?,
): Int {
return paint.measureText(text.toString().substring(start until end)).toInt()
}
override fun draw(
canvas: Canvas,
text: CharSequence,
start: Int,
end: Int,
x: Float,
top: Int,
y: Int,
bottom: Int,
paint: Paint,
) {
val originTextColor = paint.color
paint.apply {
color = strokeColor
style = Paint.Style.STROKE
this.strokeWidth = this@OutlineSpan.strokeWidth
}
canvas.drawText(text, start, end, x, y.toFloat(), paint)
paint.apply {
color = originTextColor
style = Paint.Style.FILL
}
canvas.drawText(text, start, end, x, y.toFloat(), paint)
}
}

View file

@ -1,19 +0,0 @@
package eu.kanade.tachiyomi.widget
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.source.model.Filter
import tachiyomi.domain.entries.TriStateFilter
fun Int.toTriStateFilter(): TriStateFilter {
return when (this) {
Filter.TriState.STATE_IGNORE -> TriStateFilter.DISABLED
Filter.TriState.STATE_INCLUDE -> TriStateFilter.ENABLED_IS
Filter.TriState.STATE_EXCLUDE -> TriStateFilter.ENABLED_NOT
AnimeFilter.TriState.STATE_IGNORE -> TriStateFilter.DISABLED
AnimeFilter.TriState.STATE_INCLUDE -> TriStateFilter.ENABLED_IS
AnimeFilter.TriState.STATE_EXCLUDE -> TriStateFilter.ENABLED_NOT
else -> throw IllegalStateException("Unknown TriState state: $this")
}
}

View file

@ -1,14 +0,0 @@
package eu.kanade.tachiyomi.widget.listener
import com.google.android.material.tabs.TabLayout
open class SimpleTabSelectedListener : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
}
override fun onTabUnselected(tab: TabLayout.Tab?) {
}
override fun onTabReselected(tab: TabLayout.Tab?) {
}
}

View file

@ -38,6 +38,12 @@
android:focusable="false"
android:visibility="gone" />
<View
android:id="@+id/brightness_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
<FrameLayout
android:id="@+id/reader_menu"
android:layout_width="match_parent"
@ -120,24 +126,31 @@
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/action_settings"
android:padding="@dimen/screen_edge_margin"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toStartOf="@+id/action_color_settings"
app:layout_constraintStart_toEndOf="@id/action_rotation"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_settings_24dp"
app:tint="?attr/colorOnSurface" />
<ImageButton
android:id="@+id/action_color_settings"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/custom_filter"
android:padding="@dimen/screen_edge_margin"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/action_settings"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_brightness_5_24dp"
app:tint="?attr/colorOnSurface" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</FrameLayout>
<View
android:id="@+id/brightness_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/dialog_root"
android:layout_width="match_parent"

View file

@ -1,269 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- Brightness -->
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/custom_brightness"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="16dp"
android:text="@string/pref_custom_brightness"
app:layout_constraintTop_toTopOf="parent" />
<!-- Brightness value -->
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/txt_brightness_seekbar_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:textAppearance="?attr/textAppearanceTitleSmall"
android:tint="?attr/colorOnBackground"
app:layout_constraintBottom_toBottomOf="@id/slider_brightness"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/slider_brightness"
app:srcCompat="@drawable/ic_brightness_5_24dp" />
<com.google.android.material.slider.Slider
android:id="@+id/slider_brightness"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:padding="8dp"
android:valueFrom="-75.0"
android:valueTo="100.0"
android:stepSize="1.0"
app:layout_constraintEnd_toStartOf="@id/txt_brightness_seekbar_value"
app:layout_constraintStart_toEndOf="@id/txt_brightness_seekbar_icon"
app:layout_constraintTop_toBottomOf="@id/custom_brightness" />
<TextView
android:id="@+id/txt_brightness_seekbar_value"
android:layout_width="30dp"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:textAppearance="?attr/textAppearanceTitleSmall"
app:layout_constraintBottom_toBottomOf="@id/slider_brightness"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/slider_brightness"
tools:text="50" />
<!-- Color filter -->
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_color_filter"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="16dp"
android:text="@string/pref_custom_color_filter"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/slider_brightness" />
<!-- Red filter -->
<TextView
android:id="@+id/txt_color_filter_red_symbol"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:text="@string/color_filter_r_value"
android:textAppearance="?attr/textAppearanceTitleSmall"
app:layout_constraintBottom_toBottomOf="@id/slider_color_filter_red"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/slider_color_filter_red" />
<com.google.android.material.slider.Slider
android:id="@+id/slider_color_filter_red"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:stepSize="1.0"
android:valueTo="255.0"
android:padding="8dp"
app:layout_constraintEnd_toStartOf="@id/txt_color_filter_red_value"
app:layout_constraintStart_toEndOf="@id/color_filter_symbols_barrier"
app:layout_constraintTop_toBottomOf="@id/switch_color_filter" />
<TextView
android:id="@+id/txt_color_filter_red_value"
android:layout_width="30dp"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_marginEnd="16dp"
android:textAppearance="?attr/textAppearanceTitleSmall"
app:layout_constraintBottom_toBottomOf="@id/slider_color_filter_red"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/slider_color_filter_red"
tools:text="255" />
<!-- Green filter -->
<TextView
android:id="@+id/txt_color_filter_green_symbol"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:text="@string/color_filter_g_value"
android:textAppearance="?attr/textAppearanceTitleSmall"
app:layout_constraintBottom_toBottomOf="@id/slider_color_filter_green"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/slider_color_filter_green" />
<com.google.android.material.slider.Slider
android:id="@+id/slider_color_filter_green"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:stepSize="1.0"
android:valueTo="255.0"
android:padding="8dp"
app:layout_constraintEnd_toStartOf="@id/txt_color_filter_green_value"
app:layout_constraintStart_toEndOf="@id/color_filter_symbols_barrier"
app:layout_constraintTop_toBottomOf="@id/slider_color_filter_red" />
<TextView
android:id="@+id/txt_color_filter_green_value"
android:layout_width="30dp"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_marginEnd="16dp"
android:textAppearance="?attr/textAppearanceTitleSmall"
app:layout_constraintBottom_toBottomOf="@id/slider_color_filter_green"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/slider_color_filter_green"
tools:text="255" />
<!-- Blue filter -->
<TextView
android:id="@+id/txt_color_filter_blue_symbol"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:text="@string/color_filter_b_value"
android:textAppearance="?attr/textAppearanceTitleSmall"
app:layout_constraintBottom_toBottomOf="@id/slider_color_filter_blue"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/slider_color_filter_blue" />
<com.google.android.material.slider.Slider
android:id="@+id/slider_color_filter_blue"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:stepSize="1.0"
android:valueTo="255.0"
android:padding="8dp"
app:layout_constraintEnd_toStartOf="@id/txt_color_filter_blue_value"
app:layout_constraintStart_toEndOf="@id/color_filter_symbols_barrier"
app:layout_constraintTop_toBottomOf="@id/slider_color_filter_green" />
<TextView
android:id="@+id/txt_color_filter_blue_value"
android:layout_width="30dp"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_marginEnd="16dp"
android:textAppearance="?attr/textAppearanceTitleSmall"
app:layout_constraintBottom_toBottomOf="@id/slider_color_filter_blue"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/slider_color_filter_blue"
tools:text="255" />
<!-- Alpha filter -->
<TextView
android:id="@+id/txt_color_filter_alpha_symbol"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:text="@string/color_filter_a_value"
android:textAppearance="?attr/textAppearanceTitleSmall"
app:layout_constraintBottom_toBottomOf="@id/slider_color_filter_alpha"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/slider_color_filter_alpha" />
<com.google.android.material.slider.Slider
android:id="@+id/slider_color_filter_alpha"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:stepSize="1.0"
android:valueTo="255.0"
android:padding="8dp"
app:layout_constraintEnd_toStartOf="@id/txt_color_filter_alpha_value"
app:layout_constraintStart_toEndOf="@id/color_filter_symbols_barrier"
app:layout_constraintTop_toBottomOf="@id/slider_color_filter_blue" />
<TextView
android:id="@+id/txt_color_filter_alpha_value"
android:layout_width="30dp"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_marginEnd="16dp"
android:textAppearance="?attr/textAppearanceTitleSmall"
app:layout_constraintBottom_toBottomOf="@id/slider_color_filter_alpha"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/slider_color_filter_alpha"
tools:text="255" />
<!-- Filter mode -->
<eu.kanade.tachiyomi.widget.MaterialSpinnerView
android:id="@+id/color_filter_mode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:entries="@array/color_filter_modes"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/slider_color_filter_alpha"
app:title="@string/pref_color_filter_mode" />
<!-- Grayscale -->
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/grayscale"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="16dp"
android:text="@string/pref_grayscale"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintTop_toBottomOf="@id/color_filter_mode" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/inverted_colors"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="16dp"
android:text="@string/pref_inverted_colors"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintTop_toBottomOf="@id/grayscale" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/color_filter_symbols_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="end"
app:constraint_referenced_ids="txt_color_filter_alpha_symbol,txt_color_filter_blue_symbol,txt_color_filter_red_symbol,txt_color_filter_green_symbol" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="color_filter_modes">
<item>@string/label_default</item>
<item>@string/filter_mode_multiply</item>
<item>@string/filter_mode_screen</item>
<!-- Attributes specific for SDK 28 and up -->
<item>@string/filter_mode_overlay</item>
<item>@string/filter_mode_lighten</item>
<item>@string/filter_mode_darken</item>
</string-array>
</resources>

View file

@ -67,12 +67,6 @@
<item>@string/rotation_reverse_portrait</item>
</string-array>
<string-array name="color_filter_modes">
<item>@string/label_default</item>
<item>@string/filter_mode_multiply</item>
<item>@string/filter_mode_screen</item>
</string-array>
<string-array name="invert_tapping_mode">
<item>@string/tapping_inverted_none</item>
<item>@string/tapping_inverted_horizontal</item>

View file

@ -4,11 +4,14 @@
name="cache_files"
path="." />
<external-path
name="ext_files"
name="ext"
path="." />
<external-cache-path
name="ext_cache_files"
path="." />
<external-files-path
name="ext_files"
path="." />
<!--suppress AndroidElementNotAllowed -->
<root-path
name="ext_storage"

View file

@ -12,7 +12,7 @@ import tachiyomi.core.util.system.logcat
object WebViewUtil {
const val SPOOF_PACKAGE_NAME = "org.chromium.chrome"
const val MINIMUM_WEBVIEW_VERSION = 109
const val MINIMUM_WEBVIEW_VERSION = 111
fun supportsWebView(context: Context): Boolean {
try {
@ -47,6 +47,8 @@ fun WebView.setDefaultSettings() {
builtInZoomControls = true
displayZoomControls = false
}
CookieManager.getInstance().acceptThirdPartyCookies(this)
}
private fun WebView.getWebViewMajorVersion(): Int {

View file

@ -1,5 +1,5 @@
[versions]
compiler = "1.4.7"
compiler = "1.4.8"
compose-bom = "2023.06.00-alpha01"
accompanist = "0.31.4-beta"
@ -16,7 +16,7 @@ ui-util = { module = "androidx.compose.ui:ui-util" }
material3-core = { module = "androidx.compose.material3:material3" }
material-icons = { module = "androidx.compose.material:material-icons-extended" }
# Here until M3's swipeable became public https://issuetracker.google.com/issues/234640556
# Some components aren't available in Material3
material-core = { module = "androidx.compose.material:material" }
accompanist-webview = { module = "com.google.accompanist:accompanist-webview", version.ref = "accompanist" }

View file

@ -1,13 +1,13 @@
[versions]
kotlin_version = "1.8.21"
kotlin_version = "1.8.22"
serialization_version = "1.5.1"
xml_serialization_version = "0.86.0"
xml_serialization_version = "0.86.1"
[libraries]
reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin_version" }
gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin_version" }
coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version = "1.7.1" }
coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version = "1.7.2" }
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" }
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android" }
coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava" }

View file

@ -1,10 +1,10 @@
[versions]
aboutlib_version = "10.7.0"
aboutlib_version = "10.8.0"
okhttp_version = "5.0.0-alpha.11"
shizuku_version = "12.2.0"
sqlite = "2.3.1"
sqldelight = "1.5.5"
leakcanary = "2.11"
leakcanary = "2.12"
voyager = "1.0.0-rc06"
richtext = "0.16.0"

View file

@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

5
gradlew vendored
View file

@ -130,10 +130,13 @@ location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.

View file

@ -159,7 +159,7 @@
<string name="pref_backup_flags_summary">What information to include in the backup file</string>
<string name="pref_clear_chapter_cache">Clear chapter and episode cache</string>
<string name="used_cache_both">Used by anime: %1$s, used by manga: %2$s</string>
<string name="pref_auto_clear_chapter_cache">Clear episode/chapter cache on app close</string>
<string name="pref_auto_clear_chapter_cache">Clear episode/chapter cache on app launch</string>
<string name="pref_clear_manga_database">Clear Manga database</string>
<string name="pref_clear_anime_database">Clear Anime database</string>
<string name="pref_clear_manga_database_summary">Delete history for manga that are not saved in your library</string>

View file

@ -11,6 +11,7 @@
<string name="manga">Manga</string>
<string name="chapters">Chapters</string>
<string name="track">Tracking</string>
<string name="delete_downloaded">Delete downloaded</string>
<string name="history">History</string>
<!-- Screen titles -->
@ -881,7 +882,6 @@
<!-- reserved for future use -->
<string name="update_check_eol">This Android version is no longer supported</string>
<string name="update_check_no_new_updates">No new updates available</string>
<string name="update_check_look_for_updates">Searching for updates…</string>
<!--UpdateCheck Notifications-->
<string name="update_check_notification_download_in_progress">Downloading…</string>

View file

@ -5,8 +5,13 @@ import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.AnchoredDraggableState
import androidx.compose.foundation.gestures.DraggableAnchors
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.anchoredDraggable
import androidx.compose.foundation.gestures.animateTo
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
@ -19,9 +24,6 @@ import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material.SwipeableState
import androidx.compose.material.rememberSwipeableState
import androidx.compose.material.swipeable
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
@ -39,7 +41,9 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.Velocity
@ -50,8 +54,7 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
private const val SheetAnimationDuration = 350
private val SheetAnimationSpec = tween<Float>(durationMillis = SheetAnimationDuration)
private val sheetAnimationSpec = tween<Float>(durationMillis = 350)
@Composable
fun AdaptiveSheet(
@ -61,6 +64,7 @@ fun AdaptiveSheet(
onDismissRequest: () -> Unit,
content: @Composable () -> Unit,
) {
val density = LocalDensity.current
val scope = rememberCoroutineScope()
val maxWidth = if (LocalConfiguration.current.orientation == ORIENTATION_LANDSCAPE) {
600.dp
@ -72,7 +76,7 @@ fun AdaptiveSheet(
var targetAlpha by remember { mutableFloatStateOf(0f) }
val alpha by animateFloatAsState(
targetValue = targetAlpha,
animationSpec = SheetAnimationSpec,
animationSpec = sheetAnimationSpec,
)
val internalOnDismissRequest: () -> Unit = {
scope.launch {
@ -115,23 +119,36 @@ fun AdaptiveSheet(
}
}
} else {
val swipeState = rememberSwipeableState(
initialValue = 1,
animationSpec = SheetAnimationSpec,
)
val internalOnDismissRequest: () -> Unit = { if (swipeState.currentValue == 0) scope.launch { swipeState.animateTo(1) } }
BoxWithConstraints(
val anchoredDraggableState = remember {
AnchoredDraggableState(
initialValue = 1,
animationSpec = sheetAnimationSpec,
positionalThreshold = { with(density) { 56.dp.toPx() } },
velocityThreshold = { with(density) { 125.dp.toPx() } },
)
}
val internalOnDismissRequest = {
if (anchoredDraggableState.currentValue == 0) {
scope.launch { anchoredDraggableState.animateTo(1) }
}
}
Box(
modifier = Modifier
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = internalOnDismissRequest,
)
.fillMaxSize(),
.fillMaxSize()
.onSizeChanged {
val anchors = DraggableAnchors {
0 at 0f
1 at it.height.toFloat()
}
anchoredDraggableState.updateAnchors(anchors)
},
contentAlignment = Alignment.BottomCenter,
) {
val fullHeight = constraints.maxHeight.toFloat()
val anchors = mapOf(0f to 0, fullHeight to 1)
Surface(
modifier = Modifier
.widthIn(max = maxWidth)
@ -140,26 +157,27 @@ fun AdaptiveSheet(
indication = null,
onClick = {},
)
.nestedScroll(
remember(enableSwipeDismiss, anchors) {
swipeState.preUpPostDownNestedScrollConnection(
enabled = enableSwipeDismiss,
anchor = anchors,
.then(
if (enableSwipeDismiss) {
Modifier.nestedScroll(
remember(anchoredDraggableState) {
anchoredDraggableState.preUpPostDownNestedScrollConnection()
},
)
} else {
Modifier
},
)
.offset {
IntOffset(
0,
swipeState.offset.value.roundToInt(),
anchoredDraggableState.offset.takeIf { it.isFinite() }?.roundToInt() ?: 0,
)
}
.swipeable(
enabled = enableSwipeDismiss,
state = swipeState,
anchors = anchors,
.anchoredDraggable(
state = anchoredDraggableState,
orientation = Orientation.Vertical,
resistance = null,
enabled = enableSwipeDismiss,
)
.windowInsetsPadding(
WindowInsets.systemBars
@ -168,14 +186,14 @@ fun AdaptiveSheet(
shape = MaterialTheme.shapes.extraLarge,
tonalElevation = tonalElevation,
content = {
BackHandler(enabled = swipeState.targetValue == 0, onBack = internalOnDismissRequest)
BackHandler(enabled = anchoredDraggableState.targetValue == 0, onBack = internalOnDismissRequest)
content()
},
)
LaunchedEffect(swipeState) {
scope.launch { swipeState.animateTo(0) }
snapshotFlow { swipeState.currentValue }
LaunchedEffect(anchoredDraggableState) {
scope.launch { anchoredDraggableState.animateTo(0) }
snapshotFlow { anchoredDraggableState.currentValue }
.drop(1)
.filter { it == 1 }
.collectLatest {
@ -186,17 +204,11 @@ fun AdaptiveSheet(
}
}
/**
* Yoinked from Swipeable.kt with modifications to disable
*/
private fun <T> SwipeableState<T>.preUpPostDownNestedScrollConnection(
enabled: Boolean = true,
anchor: Map<Float, T>,
) = object : NestedScrollConnection {
private fun <T> AnchoredDraggableState<T>.preUpPostDownNestedScrollConnection() = object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.toFloat()
return if (enabled && delta < 0 && source == NestedScrollSource.Drag) {
performDrag(delta).toOffset()
return if (delta < 0 && source == NestedScrollSource.Drag) {
dispatchRawDelta(delta).toOffset()
} else {
Offset.Zero
}
@ -207,17 +219,17 @@ private fun <T> SwipeableState<T>.preUpPostDownNestedScrollConnection(
available: Offset,
source: NestedScrollSource,
): Offset {
return if (enabled && source == NestedScrollSource.Drag) {
performDrag(available.toFloat()).toOffset()
return if (source == NestedScrollSource.Drag) {
dispatchRawDelta(available.toFloat()).toOffset()
} else {
Offset.Zero
}
}
override suspend fun onPreFling(available: Velocity): Velocity {
val toFling = Offset(available.x, available.y).toFloat()
return if (enabled && toFling < 0 && offset.value > anchor.keys.minOrNull()!!) {
performFling(velocity = toFling)
val toFling = available.toFloat()
return if (toFling < 0 && offset > anchors.minAnchor()) {
settle(toFling)
// since we go to the anchor with tween settling, consume all for the best UX
available
} else {
@ -226,15 +238,14 @@ private fun <T> SwipeableState<T>.preUpPostDownNestedScrollConnection(
}
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
return if (enabled) {
performFling(velocity = Offset(available.x, available.y).toFloat())
available
} else {
Velocity.Zero
}
settle(velocity = available.toFloat())
return available
}
private fun Float.toOffset(): Offset = Offset(0f, this)
@JvmName("velocityToFloat")
private fun Velocity.toFloat() = this.y
private fun Offset.toFloat(): Float = this.y
}

View file

@ -3,6 +3,7 @@ package tachiyomi.presentation.core.components
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
@ -17,6 +18,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@ -135,6 +137,43 @@ fun RadioItem(
)
}
@Composable
fun SliderItem(
label: String,
min: Int = 0,
max: Int,
value: Int,
valueText: String,
onChange: (Int) -> Unit,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(
horizontal = SettingsItemsPaddings.Horizontal,
vertical = SettingsItemsPaddings.Vertical,
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(24.dp),
) {
Column(modifier = Modifier.weight(0.5f)) {
Text(
text = label,
style = MaterialTheme.typography.bodyMedium,
)
Text(valueText)
}
Slider(
value = value.toFloat(),
onValueChange = { onChange(it.toInt()) },
modifier = Modifier.weight(1.5f),
valueRange = min.toFloat()..max.toFloat(),
steps = max - min,
)
}
}
@Composable
private fun BaseSettingsItem(
label: String,