mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-21 12:17:12 +03:00
parent
dd69ce5a12
commit
264b0e6127
60 changed files with 825 additions and 1054 deletions
|
@ -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()
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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,
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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() {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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?) {
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
3
gradle/wrapper/gradle-wrapper.properties
vendored
3
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -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
5
gradlew
vendored
|
@ -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.
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
Loading…
Reference in a new issue