fix: conflict.

Signed-off-by: KaiserBh <kaiserbh@proton.me>
This commit is contained in:
KaiserBh 2023-10-23 19:02:16 +11:00
commit fd63383d74
No known key found for this signature in database
GPG Key ID: 14D73B142042BBA9
210 changed files with 2592 additions and 1978 deletions

View File

@ -5,11 +5,6 @@
], ],
"schedule": ["every sunday"], "schedule": ["every sunday"],
"packageRules": [ "packageRules": [
{
"managers": ["maven"],
"packageNames": ["com.google.guava:guava"],
"versionScheme": "docker"
},
{ {
// Compiler plugins are tightly coupled to Kotlin version // Compiler plugins are tightly coupled to Kotlin version
"groupName": "Kotlin", "groupName": "Kotlin",

View File

@ -24,6 +24,10 @@ Before you start, please note that the ability to use following technologies is
- [Android Studio](https://developer.android.com/studio) - [Android Studio](https://developer.android.com/studio)
- Emulator or phone with developer options enabled to test changes. - Emulator or phone with developer options enabled to test changes.
## Linting
To auto-fix some linting errors, run the `ktlintFormat` Gradle task.
## Getting help ## Getting help
- Join [the Discord server](https://discord.gg/tachiyomi) for online help and to ask questions while developing. - Join [the Discord server](https://discord.gg/tachiyomi) for online help and to ask questions while developing.

View File

@ -38,7 +38,7 @@ Please make sure to read the full guidelines. Your issue may be closed without w
* Include version (More → About → Version) * Include version (More → About → Version)
* If not latest, try updating, it may have already been solved * If not latest, try updating, it may have already been solved
* Preview version is equal to the number of commits as seen in the main page * Preview version is equal to the number of commits as seen on the main page
* Include steps to reproduce (if not obvious from description) * Include steps to reproduce (if not obvious from description)
* Include screenshot (if needed) * Include screenshot (if needed)
* If it could be device-dependent, try reproducing on another device (if possible) * If it could be device-dependent, try reproducing on another device (if possible)

1
app/.gitignore vendored
View File

@ -1,4 +1,3 @@
/build /build
*iml *iml
*.iml *.iml
custom.gradle

View File

@ -192,7 +192,7 @@ dependencies {
implementation(androidx.bundles.lifecycle) implementation(androidx.bundles.lifecycle)
// Job scheduling // Job scheduling
implementation(androidx.bundles.workmanager) implementation(androidx.workmanager)
// RxJava // RxJava
implementation(libs.rxjava) implementation(libs.rxjava)

View File

@ -1,5 +1,4 @@
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android"> <shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut <shortcut
android:enabled="true" android:enabled="true"
android:icon="@drawable/sc_collections_bookmark_48dp" android:icon="@drawable/sc_collections_bookmark_48dp"

View File

@ -41,6 +41,7 @@ import tachiyomi.domain.category.interactor.UpdateCategory
import tachiyomi.domain.category.repository.CategoryRepository import tachiyomi.domain.category.repository.CategoryRepository
import tachiyomi.domain.chapter.interactor.GetChapter import tachiyomi.domain.chapter.interactor.GetChapter
import tachiyomi.domain.chapter.interactor.GetChapterByMangaId import tachiyomi.domain.chapter.interactor.GetChapterByMangaId
import tachiyomi.domain.chapter.interactor.GetChapterByUrlAndMangaId
import tachiyomi.domain.chapter.interactor.SetMangaDefaultChapterFlags import tachiyomi.domain.chapter.interactor.SetMangaDefaultChapterFlags
import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter
import tachiyomi.domain.chapter.interactor.UpdateChapter import tachiyomi.domain.chapter.interactor.UpdateChapter
@ -56,6 +57,7 @@ import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga
import tachiyomi.domain.manga.interactor.GetFavorites import tachiyomi.domain.manga.interactor.GetFavorites
import tachiyomi.domain.manga.interactor.GetLibraryManga import tachiyomi.domain.manga.interactor.GetLibraryManga
import tachiyomi.domain.manga.interactor.GetManga import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.interactor.GetMangaByUrlAndSourceId
import tachiyomi.domain.manga.interactor.GetMangaWithChapters import tachiyomi.domain.manga.interactor.GetMangaWithChapters
import tachiyomi.domain.manga.interactor.NetworkToLocalManga import tachiyomi.domain.manga.interactor.NetworkToLocalManga
import tachiyomi.domain.manga.interactor.ResetViewerFlags import tachiyomi.domain.manga.interactor.ResetViewerFlags
@ -99,6 +101,7 @@ class DomainModule : InjektModule {
addFactory { GetFavorites(get()) } addFactory { GetFavorites(get()) }
addFactory { GetLibraryManga(get()) } addFactory { GetLibraryManga(get()) }
addFactory { GetMangaWithChapters(get(), get()) } addFactory { GetMangaWithChapters(get(), get()) }
addFactory { GetMangaByUrlAndSourceId(get()) }
addFactory { GetManga(get()) } addFactory { GetManga(get()) }
addFactory { GetNextChapters(get(), get(), get()) } addFactory { GetNextChapters(get(), get(), get()) }
addFactory { ResetViewerFlags(get()) } addFactory { ResetViewerFlags(get()) }
@ -126,6 +129,7 @@ class DomainModule : InjektModule {
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) } addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
addFactory { GetChapter(get()) } addFactory { GetChapter(get()) }
addFactory { GetChapterByMangaId(get()) } addFactory { GetChapterByMangaId(get()) }
addFactory { GetChapterByUrlAndMangaId(get()) }
addFactory { UpdateChapter(get()) } addFactory { UpdateChapter(get()) }
addFactory { SetReadStatus(get(), get(), get(), get()) } addFactory { SetReadStatus(get(), get(), get(), get()) }
addFactory { ShouldUpdateDbChapter() } addFactory { ShouldUpdateDbChapter() }

View File

@ -2,6 +2,7 @@ package eu.kanade.domain.track.interactor
import android.content.Context import android.content.Context
import eu.kanade.domain.track.model.toDbTrack import eu.kanade.domain.track.model.toDbTrack
import eu.kanade.domain.track.model.toDomainTrack
import eu.kanade.domain.track.service.DelayedTrackingUpdateJob import eu.kanade.domain.track.service.DelayedTrackingUpdateJob
import eu.kanade.domain.track.store.DelayedTrackingStore import eu.kanade.domain.track.store.DelayedTrackingStore
import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.data.track.TrackerManager
@ -31,14 +32,17 @@ class TrackChapter(
return@mapNotNull null return@mapNotNull null
} }
val updatedTrack = track.copy(lastChapterRead = chapterNumber)
async { async {
runCatching { runCatching {
try { try {
val updatedTrack = service.refresh(track.toDbTrack())
.toDomainTrack(idRequired = true)!!
.copy(lastChapterRead = chapterNumber)
service.update(updatedTrack.toDbTrack(), true) service.update(updatedTrack.toDbTrack(), true)
insertTrack.await(updatedTrack) insertTrack.await(updatedTrack)
delayedTrackingStore.remove(track.id)
} catch (e: Exception) { } catch (e: Exception) {
delayedTrackingStore.addItem(updatedTrack) delayedTrackingStore.add(track.id, chapterNumber)
DelayedTrackingUpdateJob.setupTask(context) DelayedTrackingUpdateJob.setupTask(context)
throw e throw e
} }

View File

@ -8,21 +8,19 @@ import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import eu.kanade.domain.track.model.toDbTrack import eu.kanade.domain.track.interactor.TrackChapter
import eu.kanade.domain.track.store.DelayedTrackingStore import eu.kanade.domain.track.store.DelayedTrackingStore
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.util.system.workManager import eu.kanade.tachiyomi.util.system.workManager
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import tachiyomi.domain.track.interactor.GetTracks import tachiyomi.domain.track.interactor.GetTracks
import tachiyomi.domain.track.interactor.InsertTrack
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.minutes
import kotlin.time.toJavaDuration import kotlin.time.toJavaDuration
class DelayedTrackingUpdateJob(context: Context, workerParams: WorkerParameters) : class DelayedTrackingUpdateJob(private val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) { CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
@ -31,9 +29,8 @@ class DelayedTrackingUpdateJob(context: Context, workerParams: WorkerParameters)
} }
val getTracks = Injekt.get<GetTracks>() val getTracks = Injekt.get<GetTracks>()
val insertTrack = Injekt.get<InsertTrack>() val trackChapter = Injekt.get<TrackChapter>()
val trackerManager = Injekt.get<TrackerManager>()
val delayedTrackingStore = Injekt.get<DelayedTrackingStore>() val delayedTrackingStore = Injekt.get<DelayedTrackingStore>()
withIOContext { withIOContext {
@ -46,17 +43,8 @@ class DelayedTrackingUpdateJob(context: Context, workerParams: WorkerParameters)
track?.copy(lastChapterRead = it.lastChapterRead.toDouble()) track?.copy(lastChapterRead = it.lastChapterRead.toDouble())
} }
.forEach { track -> .forEach { track ->
try { logcat(LogPriority.DEBUG) { "Updating delayed track item: ${track.mangaId}, last chapter read: ${track.lastChapterRead}" }
val service = trackerManager.get(track.syncId) trackChapter.await(context, track.mangaId, track.lastChapterRead)
if (service != null && service.isLoggedIn) {
logcat(LogPriority.DEBUG) { "Updating delayed track item: ${track.id}, last chapter read: ${track.lastChapterRead}" }
service.update(track.toDbTrack(), true)
insertTrack.await(track)
}
delayedTrackingStore.remove(track.id)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
}
} }
} }

View File

@ -4,7 +4,6 @@ import android.content.Context
import androidx.core.content.edit import androidx.core.content.edit
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import tachiyomi.domain.track.model.Track
class DelayedTrackingStore(context: Context) { class DelayedTrackingStore(context: Context) {
@ -13,13 +12,12 @@ class DelayedTrackingStore(context: Context) {
*/ */
private val preferences = context.getSharedPreferences("tracking_queue", Context.MODE_PRIVATE) private val preferences = context.getSharedPreferences("tracking_queue", Context.MODE_PRIVATE)
fun addItem(track: Track) { fun add(trackId: Long, lastChapterRead: Double) {
val trackId = track.id.toString() val previousLastChapterRead = preferences.getFloat(trackId.toString(), 0f)
val lastChapterRead = preferences.getFloat(trackId, 0f) if (lastChapterRead > previousLastChapterRead) {
if (track.lastChapterRead > lastChapterRead) { logcat(LogPriority.DEBUG) { "Queuing track item: $trackId, last chapter read: $lastChapterRead" }
logcat(LogPriority.DEBUG) { "Queuing track item: $trackId, last chapter read: ${track.lastChapterRead}" }
preferences.edit { preferences.edit {
putFloat(trackId, track.lastChapterRead.toFloat()) putFloat(trackId.toString(), lastChapterRead.toFloat())
} }
} }
} }

View File

@ -4,7 +4,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.HelpOutline import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.outlined.Public import androidx.compose.material.icons.outlined.Public
import androidx.compose.material.icons.outlined.Refresh import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarDuration
@ -79,7 +79,7 @@ fun BrowseSourceContent(
listOf( listOf(
EmptyScreenAction( EmptyScreenAction(
stringResId = R.string.local_source_help_guide, stringResId = R.string.local_source_help_guide,
icon = Icons.Outlined.HelpOutline, icon = Icons.AutoMirrored.Outlined.HelpOutline,
onClick = onLocalSourceHelpClick, onClick = onLocalSourceHelpClick,
), ),
) )
@ -97,7 +97,7 @@ fun BrowseSourceContent(
), ),
EmptyScreenAction( EmptyScreenAction(
stringResId = R.string.label_help, stringResId = R.string.label_help,
icon = Icons.Outlined.HelpOutline, icon = Icons.AutoMirrored.Outlined.HelpOutline,
onClick = onHelpClick, onClick = onHelpClick,
), ),
) )

View File

@ -16,7 +16,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.HelpOutline import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.outlined.History import androidx.compose.material.icons.outlined.History
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
@ -92,7 +92,7 @@ fun ExtensionDetailsScreen(
add( add(
AppBar.Action( AppBar.Action(
title = stringResource(R.string.action_faq_and_guides), title = stringResource(R.string.action_faq_and_guides),
icon = Icons.Outlined.HelpOutline, icon = Icons.AutoMirrored.Outlined.HelpOutline,
onClick = onClickReadme, onClick = onClickReadme,
), ),
) )

View File

@ -244,7 +244,10 @@ private fun ExtensionItem(
) )
} }
val padding by animateDpAsState(targetValue = if (idle) 0.dp else 8.dp) val padding by animateDpAsState(
targetValue = if (idle) 0.dp else 8.dp,
label = "iconPadding",
)
ExtensionIcon( ExtensionIcon(
extension = extension, extension = extension,
modifier = Modifier modifier = Modifier

View File

@ -1,7 +1,7 @@
package eu.kanade.presentation.browse.components package eu.kanade.presentation.browse.components
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ViewList import androidx.compose.material.icons.automirrored.filled.ViewList
import androidx.compose.material.icons.filled.ViewModule import androidx.compose.material.icons.filled.ViewModule
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.TopAppBarScrollBehavior
@ -56,7 +56,7 @@ fun BrowseSourceToolbar(
actions = listOfNotNull( actions = listOfNotNull(
AppBar.Action( AppBar.Action(
title = stringResource(R.string.action_display_mode), title = stringResource(R.string.action_display_mode),
icon = if (displayMode == LibraryDisplayMode.List) Icons.Filled.ViewList else Icons.Filled.ViewModule, icon = if (displayMode == LibraryDisplayMode.List) Icons.AutoMirrored.Filled.ViewList else Icons.Filled.ViewModule,
onClick = { selectingDisplayMode = true }, onClick = { selectingDisplayMode = true },
), ),
if (isLocalSource) { if (isLocalSource) {

View File

@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
import androidx.compose.material.icons.outlined.ArrowForward import androidx.compose.material.icons.outlined.ArrowForward
import androidx.compose.material.icons.outlined.Error import androidx.compose.material.icons.outlined.Error
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
@ -54,7 +55,7 @@ fun GlobalSearchResultItem(
Text(text = subtitle) Text(text = subtitle)
} }
IconButton(onClick = onClick) { IconButton(onClick = onClick) {
Icon(imageVector = Icons.Outlined.ArrowForward, contentDescription = null) Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null)
} }
} }
content() content()

View File

@ -58,7 +58,7 @@ fun GlobalSearchToolbar(
) )
if (progress in 1..<total) { if (progress in 1..<total) {
LinearProgressIndicator( LinearProgressIndicator(
progress = progress / total.toFloat(), progress = { progress / total.toFloat() },
modifier = Modifier modifier = Modifier
.align(Alignment.BottomStart) .align(Alignment.BottomStart)
.fillMaxWidth(), .fillMaxWidth(),

View File

@ -8,17 +8,11 @@ import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material.icons.outlined.SortByAlpha import androidx.compose.material.icons.outlined.SortByAlpha
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.category.components.CategoryFloatingActionButton import eu.kanade.presentation.category.components.CategoryFloatingActionButton
import eu.kanade.presentation.category.components.CategoryListItem import eu.kanade.presentation.category.components.CategoryListItem
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
@ -46,21 +40,9 @@ fun CategoryScreen(
val lazyListState = rememberLazyListState() val lazyListState = rememberLazyListState()
Scaffold( Scaffold(
topBar = { scrollBehavior -> topBar = { scrollBehavior ->
TopAppBar( AppBar(
title = { title = stringResource(R.string.action_edit_categories),
Text( navigateUp = navigateUp,
text = stringResource(R.string.action_edit_categories),
modifier = Modifier.padding(start = 8.dp),
)
},
navigationIcon = {
IconButton(onClick = navigateUp) {
Icon(
imageVector = Icons.Outlined.ArrowBack,
contentDescription = stringResource(R.string.abc_action_bar_up_description),
)
}
},
actions = { actions = {
AppBarActions( AppBarActions(
listOf( listOf(

View File

@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.Label
import androidx.compose.material.icons.outlined.ArrowDropDown import androidx.compose.material.icons.outlined.ArrowDropDown
import androidx.compose.material.icons.outlined.ArrowDropUp import androidx.compose.material.icons.outlined.ArrowDropUp
import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Delete
@ -49,7 +50,7 @@ fun CategoryListItem(
), ),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Icon(imageVector = Icons.Outlined.Label, contentDescription = "") Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = "")
Text( Text(
text = category.name, text = category.name,
modifier = Modifier modifier = Modifier

View File

@ -1,5 +1,6 @@
package eu.kanade.presentation.components package eu.kanade.presentation.components
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
@ -9,8 +10,7 @@ import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.TextFieldDefaults import androidx.compose.material.TextFieldDefaults
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowBack import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material.icons.outlined.ArrowForward
import androidx.compose.material.icons.outlined.Close import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material.icons.outlined.Search import androidx.compose.material.icons.outlined.Search
@ -39,14 +39,12 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@ -60,6 +58,7 @@ const val SEARCH_DEBOUNCE_MILLIS = 250L
@Composable @Composable
fun AppBar( fun AppBar(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
backgroundColor: Color? = null,
// Text // Text
title: String?, title: String?,
subtitle: String? = null, subtitle: String? = null,
@ -81,6 +80,7 @@ fun AppBar(
AppBar( AppBar(
modifier = modifier, modifier = modifier,
backgroundColor = backgroundColor,
titleContent = { titleContent = {
if (isActionMode) { if (isActionMode) {
AppBarTitle(actionModeCounter.toString()) AppBarTitle(actionModeCounter.toString())
@ -106,6 +106,7 @@ fun AppBar(
@Composable @Composable
fun AppBar( fun AppBar(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
backgroundColor: Color? = null,
// Title // Title
titleContent: @Composable () -> Unit, titleContent: @Composable () -> Unit,
// Up button // Up button
@ -142,7 +143,7 @@ fun AppBar(
title = titleContent, title = titleContent,
actions = actions, actions = actions,
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation( containerColor = backgroundColor ?: MaterialTheme.colorScheme.surfaceColorAtElevation(
elevation = if (isActionMode) 3.dp else 0.dp, elevation = if (isActionMode) 3.dp else 0.dp,
), ),
), ),
@ -170,6 +171,9 @@ fun AppBarTitle(
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = Modifier.basicMarquee(
delayMillis = 2_000,
),
) )
} }
} }
@ -363,7 +367,7 @@ fun SearchToolbar(
@Composable @Composable
fun UpIcon(navigationIcon: ImageVector? = null) { fun UpIcon(navigationIcon: ImageVector? = null) {
val icon = navigationIcon val icon = navigationIcon
?: if (LocalLayoutDirection.current == LayoutDirection.Ltr) Icons.Outlined.ArrowBack else Icons.Outlined.ArrowForward ?: Icons.AutoMirrored.Outlined.ArrowBack
Icon( Icon(
imageVector = icon, imageVector = icon,
contentDescription = stringResource(R.string.abc_action_bar_up_description), contentDescription = stringResource(R.string.abc_action_bar_up_description),

View File

@ -3,8 +3,7 @@ package eu.kanade.presentation.components
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.sizeIn
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowLeft import androidx.compose.material.icons.automirrored.outlined.ArrowRight
import androidx.compose.material.icons.outlined.ArrowRight
import androidx.compose.material.icons.outlined.RadioButtonChecked import androidx.compose.material.icons.outlined.RadioButtonChecked
import androidx.compose.material.icons.outlined.RadioButtonUnchecked import androidx.compose.material.icons.outlined.RadioButtonUnchecked
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
@ -16,10 +15,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.PopupProperties import androidx.compose.ui.window.PopupProperties
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@ -77,14 +74,13 @@ fun NestedMenuItem(
) { ) {
var nestedExpanded by remember { mutableStateOf(false) } var nestedExpanded by remember { mutableStateOf(false) }
val closeMenu = { nestedExpanded = false } val closeMenu = { nestedExpanded = false }
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
DropdownMenuItem( DropdownMenuItem(
text = text, text = text,
onClick = { nestedExpanded = true }, onClick = { nestedExpanded = true },
trailingIcon = { trailingIcon = {
Icon( Icon(
imageVector = if (isLtr) Icons.Outlined.ArrowRight else Icons.Outlined.ArrowLeft, imageVector = Icons.AutoMirrored.Outlined.ArrowRight,
contentDescription = null, contentDescription = null,
) )
}, },

View File

@ -1,6 +1,7 @@
package eu.kanade.presentation.components package eu.kanade.presentation.components
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.outlined.HelpOutline import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.Refresh import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
@ -38,7 +39,7 @@ private fun WithActionPreview() {
), ),
EmptyScreenAction( EmptyScreenAction(
stringResId = R.string.getting_started_guide, stringResId = R.string.getting_started_guide,
icon = Icons.Outlined.HelpOutline, icon = Icons.AutoMirrored.Outlined.HelpOutline,
onClick = {}, onClick = {},
), ),
), ),

View File

@ -14,8 +14,8 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -55,7 +55,7 @@ fun TabbedDialog(
Column { Column {
Row { Row {
TabRow( PrimaryTabRow(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
selectedTabIndex = pagerState.currentPage, selectedTabIndex = pagerState.currentPage,
indicator = { TabIndicator(it[pagerState.currentPage], pagerState.currentPageOffsetFraction) }, indicator = { TabIndicator(it[pagerState.currentPage], pagerState.currentPageOffsetFraction) },

View File

@ -9,10 +9,10 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -67,7 +67,7 @@ fun TabbedScreen(
end = contentPadding.calculateEndPadding(LocalLayoutDirection.current), end = contentPadding.calculateEndPadding(LocalLayoutDirection.current),
), ),
) { ) {
TabRow( PrimaryTabRow(
selectedTabIndex = state.currentPage, selectedTabIndex = state.currentPage,
indicator = { TabIndicator(it[state.currentPage], state.currentPageOffsetFraction) }, indicator = { TabIndicator(it[state.currentPage], state.currentPageOffsetFraction) },
) { ) {

View File

@ -1,18 +0,0 @@
package eu.kanade.presentation.extensions
import android.Manifest
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import com.google.accompanist.permissions.rememberPermissionState
import eu.kanade.tachiyomi.util.storage.DiskUtil
/**
* Launches request for [Manifest.permission.WRITE_EXTERNAL_STORAGE] permission
*/
@Composable
fun DiskUtil.RequestStoragePermission() {
val permissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE)
LaunchedEffect(Unit) {
permissionState.launchPermissionRequest()
}
}

View File

@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import eu.kanade.domain.ui.UiPreferences import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.AppBarActions
@ -18,13 +19,16 @@ import eu.kanade.presentation.components.AppBarTitle
import eu.kanade.presentation.components.RelativeDateHeader import eu.kanade.presentation.components.RelativeDateHeader
import eu.kanade.presentation.components.SearchToolbar import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.presentation.history.components.HistoryItem import eu.kanade.presentation.history.components.HistoryItem
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.history.HistoryScreenModel import eu.kanade.tachiyomi.ui.history.HistoryScreenModel
import tachiyomi.core.preference.InMemoryPreferenceStore
import tachiyomi.domain.history.model.HistoryWithRelations import tachiyomi.domain.history.model.HistoryWithRelations
import tachiyomi.presentation.core.components.FastScrollLazyColumn import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen import tachiyomi.presentation.core.screens.LoadingScreen
import tachiyomi.presentation.core.util.ThemePreviews
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.Date import java.util.Date
@ -37,6 +41,7 @@ fun HistoryScreen(
onClickCover: (mangaId: Long) -> Unit, onClickCover: (mangaId: Long) -> Unit,
onClickResume: (mangaId: Long, chapterId: Long) -> Unit, onClickResume: (mangaId: Long, chapterId: Long) -> Unit,
onDialogChange: (HistoryScreenModel.Dialog?) -> Unit, onDialogChange: (HistoryScreenModel.Dialog?) -> Unit,
preferences: UiPreferences = Injekt.get(),
) { ) {
Scaffold( Scaffold(
topBar = { scrollBehavior -> topBar = { scrollBehavior ->
@ -82,6 +87,7 @@ fun HistoryScreen(
onClickCover = { history -> onClickCover(history.mangaId) }, onClickCover = { history -> onClickCover(history.mangaId) },
onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) }, onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) },
onClickDelete = { item -> onDialogChange(HistoryScreenModel.Dialog.Delete(item)) }, onClickDelete = { item -> onDialogChange(HistoryScreenModel.Dialog.Delete(item)) },
preferences = preferences,
) )
} }
} }
@ -95,7 +101,7 @@ private fun HistoryScreenContent(
onClickCover: (HistoryWithRelations) -> Unit, onClickCover: (HistoryWithRelations) -> Unit,
onClickResume: (HistoryWithRelations) -> Unit, onClickResume: (HistoryWithRelations) -> Unit,
onClickDelete: (HistoryWithRelations) -> Unit, onClickDelete: (HistoryWithRelations) -> Unit,
preferences: UiPreferences = Injekt.get(), preferences: UiPreferences,
) { ) {
val relativeTime = remember { preferences.relativeTime().get() } val relativeTime = remember { preferences.relativeTime().get() }
val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) } val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
@ -141,3 +147,32 @@ sealed interface HistoryUiModel {
data class Header(val date: Date) : HistoryUiModel data class Header(val date: Date) : HistoryUiModel
data class Item(val item: HistoryWithRelations) : HistoryUiModel data class Item(val item: HistoryWithRelations) : HistoryUiModel
} }
@ThemePreviews
@Composable
internal fun HistoryScreenPreviews(
@PreviewParameter(HistoryScreenModelStateProvider::class)
historyState: HistoryScreenModel.State,
) {
TachiyomiTheme {
HistoryScreen(
state = historyState,
snackbarHostState = SnackbarHostState(),
onSearchQueryChange = {},
onClickCover = {},
onClickResume = { _, _ -> run {} },
onDialogChange = {},
preferences = UiPreferences(
InMemoryPreferenceStore(
sequenceOf(
InMemoryPreferenceStore.InMemoryPreference(
key = "relative_time_v2",
data = false,
defaultValue = false,
),
),
),
),
)
}
}

View File

@ -0,0 +1,109 @@
package eu.kanade.presentation.history
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import eu.kanade.tachiyomi.ui.history.HistoryScreenModel
import tachiyomi.domain.history.model.HistoryWithRelations
import tachiyomi.domain.manga.model.MangaCover
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.Date
import kotlin.random.Random
class HistoryScreenModelStateProvider : PreviewParameterProvider<HistoryScreenModel.State> {
private val multiPage = HistoryScreenModel.State(
searchQuery = null,
list =
listOf(HistoryUiModelExamples.headerToday)
.asSequence()
.plus(HistoryUiModelExamples.items().take(3))
.plus(HistoryUiModelExamples.header { it.minus(1, ChronoUnit.DAYS) })
.plus(HistoryUiModelExamples.items().take(1))
.plus(HistoryUiModelExamples.header { it.minus(2, ChronoUnit.DAYS) })
.plus(HistoryUiModelExamples.items().take(7))
.toList(),
dialog = null,
)
private val shortRecent = HistoryScreenModel.State(
searchQuery = null,
list = listOf(
HistoryUiModelExamples.headerToday,
HistoryUiModelExamples.items().first(),
),
dialog = null,
)
private val shortFuture = HistoryScreenModel.State(
searchQuery = null,
list = listOf(
HistoryUiModelExamples.headerTomorrow,
HistoryUiModelExamples.items().first(),
),
dialog = null,
)
private val empty = HistoryScreenModel.State(
searchQuery = null,
list = listOf(),
dialog = null,
)
private val loadingWithSearchQuery = HistoryScreenModel.State(
searchQuery = "Example Search Query",
)
private val loading = HistoryScreenModel.State(
searchQuery = null,
list = null,
dialog = null,
)
override val values: Sequence<HistoryScreenModel.State> = sequenceOf(
multiPage,
shortRecent,
shortFuture,
empty,
loadingWithSearchQuery,
loading,
)
private object HistoryUiModelExamples {
val headerToday = header()
val headerTomorrow =
HistoryUiModel.Header(Date.from(Instant.now().plus(1, ChronoUnit.DAYS)))
fun header(instantBuilder: (Instant) -> Instant = { it }) =
HistoryUiModel.Header(Date.from(instantBuilder(Instant.now())))
fun items() = sequence {
var count = 1
while (true) {
yield(randItem { it.copy(title = "Example Title $count") })
count += 1
}
}
fun randItem(historyBuilder: (HistoryWithRelations) -> HistoryWithRelations = { it }) =
HistoryUiModel.Item(
historyBuilder(
HistoryWithRelations(
id = Random.nextLong(),
chapterId = Random.nextLong(),
mangaId = Random.nextLong(),
title = "Test Title",
chapterNumber = Random.nextDouble(),
readAt = Date.from(Instant.now()),
readDuration = Random.nextLong(),
coverData = MangaCover(
mangaId = Random.nextLong(),
sourceId = Random.nextLong(),
isMangaFavorite = Random.nextBoolean(),
url = "https://example.com/cover.png",
lastModified = Random.nextLong(),
),
),
),
)
}
}

View File

@ -0,0 +1,13 @@
package eu.kanade.presentation.history
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import java.time.Instant
import java.util.Date
object HistoryUiModelProviders {
class HeadNow : PreviewParameterProvider<HistoryUiModel> {
override val values: Sequence<HistoryUiModel> =
sequenceOf(HistoryUiModel.Header(Date.from(Instant.now())))
}
}

View File

@ -1,12 +1,8 @@
package eu.kanade.presentation.history.components package eu.kanade.presentation.history.components
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.selection.toggleable
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -14,11 +10,12 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.util.ThemePreviews
@Composable @Composable
fun HistoryDeleteDialog( fun HistoryDeleteDialog(
@ -32,28 +29,16 @@ fun HistoryDeleteDialog(
Text(text = stringResource(R.string.action_remove)) Text(text = stringResource(R.string.action_remove))
}, },
text = { text = {
Column { Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(text = stringResource(R.string.dialog_with_checkbox_remove_description)) Text(text = stringResource(R.string.dialog_with_checkbox_remove_description))
Row(
modifier = Modifier LabeledCheckbox(
.padding(top = 16.dp) label = stringResource(R.string.dialog_with_checkbox_reset),
.toggleable( checked = removeEverything,
interactionSource = remember { MutableInteractionSource() }, onCheckedChange = { removeEverything = it },
indication = null, )
value = removeEverything,
onValueChange = { removeEverything = it },
),
verticalAlignment = Alignment.CenterVertically,
) {
Checkbox(
checked = removeEverything,
onCheckedChange = null,
)
Text(
modifier = Modifier.padding(start = 4.dp),
text = stringResource(R.string.dialog_with_checkbox_reset),
)
}
} }
}, },
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
@ -101,3 +86,14 @@ fun HistoryDeleteAllDialog(
}, },
) )
} }
@ThemePreviews
@Composable
private fun HistoryDeleteDialogPreview() {
TachiyomiTheme {
HistoryDeleteDialog(
onDismissRequest = {},
onDelete = {},
)
}
}

View File

@ -19,15 +19,18 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.presentation.manga.components.MangaCover import eu.kanade.presentation.manga.components.MangaCover
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.presentation.util.formatChapterNumber import eu.kanade.presentation.util.formatChapterNumber
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.lang.toTimestampString import eu.kanade.tachiyomi.util.lang.toTimestampString
import tachiyomi.domain.history.model.HistoryWithRelations import tachiyomi.domain.history.model.HistoryWithRelations
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.util.ThemePreviews
private val HISTORY_ITEM_HEIGHT = 96.dp private val HistoryItemHeight = 96.dp
@Composable @Composable
fun HistoryItem( fun HistoryItem(
@ -40,7 +43,7 @@ fun HistoryItem(
Row( Row(
modifier = modifier modifier = modifier
.clickable(onClick = onClickResume) .clickable(onClick = onClickResume)
.height(HISTORY_ITEM_HEIGHT) .height(HistoryItemHeight)
.padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small), .padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
@ -87,3 +90,19 @@ fun HistoryItem(
} }
} }
} }
@ThemePreviews
@Composable
private fun HistoryItemPreviews(
@PreviewParameter(HistoryWithRelationsProvider::class)
historyWithRelations: HistoryWithRelations,
) {
TachiyomiTheme {
HistoryItem(
history = historyWithRelations,
onClickCover = {},
onClickResume = {},
onClickDelete = {},
)
}
}

View File

@ -0,0 +1,62 @@
package eu.kanade.presentation.history.components
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import tachiyomi.domain.history.model.HistoryWithRelations
import java.util.Date
internal class HistoryWithRelationsProvider : PreviewParameterProvider<HistoryWithRelations> {
private val simple = HistoryWithRelations(
id = 1L,
chapterId = 2L,
mangaId = 3L,
title = "Test Title",
chapterNumber = 10.2,
readAt = Date(1697247357L),
readDuration = 123L,
coverData = tachiyomi.domain.manga.model.MangaCover(
mangaId = 3L,
sourceId = 4L,
isMangaFavorite = false,
url = "https://example.com/cover.png",
lastModified = 5L,
),
)
private val historyWithoutReadAt = HistoryWithRelations(
id = 1L,
chapterId = 2L,
mangaId = 3L,
title = "Test Title",
chapterNumber = 10.2,
readAt = null,
readDuration = 123L,
coverData = tachiyomi.domain.manga.model.MangaCover(
mangaId = 3L,
sourceId = 4L,
isMangaFavorite = false,
url = "https://example.com/cover.png",
lastModified = 5L,
),
)
private val historyWithNegativeChapterNumber = HistoryWithRelations(
id = 1L,
chapterId = 2L,
mangaId = 3L,
title = "Test Title",
chapterNumber = -2.0,
readAt = Date(1697247357L),
readDuration = 123L,
coverData = tachiyomi.domain.manga.model.MangaCover(
mangaId = 3L,
sourceId = 4L,
isMangaFavorite = false,
url = "https://example.com/cover.png",
lastModified = 5L,
),
)
override val values: Sequence<HistoryWithRelations>
get() = sequenceOf(simple, historyWithoutReadAt, historyWithNegativeChapterNumber)
}

View File

@ -1,11 +1,7 @@
package eu.kanade.presentation.library package eu.kanade.presentation.library
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -13,11 +9,10 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import tachiyomi.core.preference.CheckboxState import tachiyomi.core.preference.CheckboxState
import tachiyomi.presentation.core.components.LabeledCheckbox
@Composable @Composable
fun DeleteLibraryMangaDialog( fun DeleteLibraryMangaDialog(
@ -62,27 +57,18 @@ fun DeleteLibraryMangaDialog(
text = { text = {
Column { Column {
list.forEach { state -> list.forEach { state ->
val onCheck = { LabeledCheckbox(
val index = list.indexOf(state) label = stringResource(state.value),
if (index != -1) { checked = state.isChecked,
val mutableList = list.toMutableList() onCheckedChange = {
mutableList[index] = state.next() as CheckboxState.State<Int> val index = list.indexOf(state)
list = mutableList.toList() if (index != -1) {
} val mutableList = list.toMutableList()
} mutableList[index] = state.next() as CheckboxState.State<Int>
list = mutableList.toList()
Row( }
modifier = Modifier },
.fillMaxWidth() )
.clickable { onCheck() },
verticalAlignment = Alignment.CenterVertically,
) {
Checkbox(
checked = state.isChecked,
onCheckedChange = { onCheck() },
)
Text(text = stringResource(state.value))
}
} }
} }
}, },

View File

@ -4,7 +4,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.PagerState
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ScrollableTabRow import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -21,7 +21,7 @@ internal fun LibraryTabs(
onTabItemClick: (Int) -> Unit, onTabItemClick: (Int) -> Unit,
) { ) {
Column { Column {
ScrollableTabRow( PrimaryScrollableTabRow(
selectedTabIndex = pagerState.currentPage, selectedTabIndex = pagerState.currentPage,
edgePadding = 0.dp, edgePadding = 0.dp,
indicator = { TabIndicator(it[pagerState.currentPage], pagerState.currentPageOffsetFraction) }, indicator = { TabIndicator(it[pagerState.currentPage], pagerState.currentPageOffsetFraction) },

View File

@ -1,16 +1,12 @@
package eu.kanade.presentation.manga package eu.kanade.presentation.manga
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope 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.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
@ -19,7 +15,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -30,6 +25,7 @@ import eu.kanade.presentation.components.TabbedDialogPaddings
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import tachiyomi.core.preference.TriState import tachiyomi.core.preference.TriState
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.RadioItem import tachiyomi.presentation.core.components.RadioItem
import tachiyomi.presentation.core.components.SortItem import tachiyomi.presentation.core.components.SortItem
import tachiyomi.presentation.core.components.TriStateItem import tachiyomi.presentation.core.components.TriStateItem
@ -172,6 +168,7 @@ private fun SetAsDefaultDialog(
onConfirmed: (optionalChecked: Boolean) -> Unit, onConfirmed: (optionalChecked: Boolean) -> Unit,
) { ) {
var optionalChecked by rememberSaveable { mutableStateOf(false) } var optionalChecked by rememberSaveable { mutableStateOf(false) }
AlertDialog( AlertDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(R.string.chapter_settings)) }, title = { Text(text = stringResource(R.string.chapter_settings)) },
@ -181,25 +178,16 @@ private fun SetAsDefaultDialog(
) { ) {
Text(text = stringResource(R.string.confirm_set_chapter_settings)) Text(text = stringResource(R.string.confirm_set_chapter_settings))
Row( LabeledCheckbox(
modifier = Modifier label = stringResource(R.string.also_set_chapter_settings_for_library),
.clickable { optionalChecked = !optionalChecked } checked = optionalChecked,
.padding(vertical = 8.dp) onCheckedChange = { optionalChecked = it },
.fillMaxWidth(), )
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Checkbox(
checked = optionalChecked,
onCheckedChange = null,
)
Text(text = stringResource(R.string.also_set_chapter_settings_for_library))
}
} }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismissRequest) { TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel)) Text(text = stringResource(R.string.action_cancel))
} }
}, },
confirmButton = { confirmButton = {

View File

@ -140,6 +140,7 @@ private fun DownloadingIndicator(
val animatedProgress by animateFloatAsState( val animatedProgress by animateFloatAsState(
targetValue = downloadProgress / 100f, targetValue = downloadProgress / 100f,
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec, animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
label = "progress",
) )
arrowColor = if (animatedProgress < 0.5f) { arrowColor = if (animatedProgress < 0.5f) {
strokeColor strokeColor
@ -147,7 +148,7 @@ private fun DownloadingIndicator(
MaterialTheme.colorScheme.background MaterialTheme.colorScheme.background
} }
CircularProgressIndicator( CircularProgressIndicator(
progress = animatedProgress, progress = { animatedProgress },
modifier = IndicatorModifier, modifier = IndicatorModifier,
color = strokeColor, color = strokeColor,
strokeWidth = IndicatorSize / 2, strokeWidth = IndicatorSize / 2,

View File

@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.shape.ZeroCornerSize import androidx.compose.foundation.shape.ZeroCornerSize
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.Label
import androidx.compose.material.icons.outlined.BookmarkAdd import androidx.compose.material.icons.outlined.BookmarkAdd
import androidx.compose.material.icons.outlined.BookmarkRemove import androidx.compose.material.icons.outlined.BookmarkRemove
import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Delete
@ -258,7 +259,7 @@ fun LibraryBottomActionMenu(
) { ) {
Button( Button(
title = stringResource(R.string.action_move_category), title = stringResource(R.string.action_move_category),
icon = Icons.Outlined.Label, icon = Icons.AutoMirrored.Outlined.Label,
toConfirm = confirm[0], toConfirm = confirm[0],
onLongClick = { onLongClickItem(0) }, onLongClick = { onLongClickItem(0) },
onClick = onChangeCategoryClicked, onClick = onChangeCategoryClicked,

View File

@ -48,7 +48,6 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
@ -63,9 +62,8 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.SubcomposeLayout import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@ -589,67 +587,70 @@ private fun MangaSummary(
expanded: Boolean, expanded: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
var expandedHeight by remember { mutableIntStateOf(0) }
var shrunkHeight by remember { mutableIntStateOf(0) }
val heightDelta = remember(expandedHeight, shrunkHeight) { expandedHeight - shrunkHeight }
val animProgress by animateFloatAsState(if (expanded) 1f else 0f) val animProgress by animateFloatAsState(if (expanded) 1f else 0f)
val scrimHeight = with(LocalDensity.current) { remember { 24.sp.roundToPx() } } Layout(
modifier = modifier.clipToBounds(),
SubcomposeLayout(modifier = modifier.clipToBounds()) { constraints -> contents = listOf(
val shrunkPlaceable = subcompose("description-s") { {
Text(
text = "\n\n", // Shows at least 3 lines
style = MaterialTheme.typography.bodyMedium,
)
}.map { it.measure(constraints) }
shrunkHeight = shrunkPlaceable.maxByOrNull { it.height }?.height ?: 0
val expandedPlaceable = subcompose("description-l") {
Text(
text = expandedDescription,
style = MaterialTheme.typography.bodyMedium,
)
}.map { it.measure(constraints) }
expandedHeight = expandedPlaceable.maxByOrNull { it.height }?.height?.coerceAtLeast(shrunkHeight) ?: 0
val actualPlaceable = subcompose("description") {
SelectionContainer {
Text( Text(
text = if (expanded) expandedDescription else shrunkDescription, text = "\n\n", // Shows at least 3 lines
maxLines = Int.MAX_VALUE,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.secondaryItemAlpha(),
) )
} },
}.map { it.measure(constraints) } {
Text(
text = expandedDescription,
style = MaterialTheme.typography.bodyMedium,
)
},
{
SelectionContainer {
Text(
text = if (expanded) expandedDescription else shrunkDescription,
maxLines = Int.MAX_VALUE,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.secondaryItemAlpha(),
)
}
},
{
val colors = listOf(Color.Transparent, MaterialTheme.colorScheme.background)
Box(
modifier = Modifier.background(Brush.verticalGradient(colors = colors)),
contentAlignment = Alignment.Center,
) {
val image = AnimatedImageVector.animatedVectorResource(R.drawable.anim_caret_down)
Icon(
painter = rememberAnimatedVectorPainter(image, !expanded),
contentDescription = stringResource(if (expanded) R.string.manga_info_collapse else R.string.manga_info_expand),
tint = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.background(Brush.radialGradient(colors = colors.asReversed())),
)
}
},
),
) { (shrunk, expanded, actual, scrim), constraints ->
val shrunkHeight = shrunk.single()
.measure(constraints)
.height
val expandedHeight = expanded.single()
.measure(constraints)
.height
val heightDelta = expandedHeight - shrunkHeight
val scrimHeight = 24.dp.roundToPx()
val scrimPlaceable = subcompose("scrim") { val actualPlaceable = actual.single()
val colors = listOf(Color.Transparent, MaterialTheme.colorScheme.background) .measure(constraints)
Box( val scrimPlaceable = scrim.single()
modifier = Modifier.background(Brush.verticalGradient(colors = colors)), .measure(Constraints.fixed(width = constraints.maxWidth, height = scrimHeight))
contentAlignment = Alignment.Center,
) {
val image = AnimatedImageVector.animatedVectorResource(R.drawable.anim_caret_down)
Icon(
painter = rememberAnimatedVectorPainter(image, !expanded),
contentDescription = stringResource(if (expanded) R.string.manga_info_collapse else R.string.manga_info_expand),
tint = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.background(Brush.radialGradient(colors = colors.asReversed())),
)
}
}.map { it.measure(Constraints.fixed(width = constraints.maxWidth, height = scrimHeight)) }
val currentHeight = shrunkHeight + ((heightDelta + scrimHeight) * animProgress).roundToInt() val currentHeight = shrunkHeight + ((heightDelta + scrimHeight) * animProgress).roundToInt()
layout(constraints.maxWidth, currentHeight) { layout(constraints.maxWidth, currentHeight) {
actualPlaceable.forEach { actualPlaceable.place(0, 0)
it.place(0, 0)
}
val scrimY = currentHeight - scrimHeight val scrimY = currentHeight - scrimHeight
scrimPlaceable.forEach { scrimPlaceable.place(0, scrimY)
it.place(0, scrimY)
}
} }
} }
} }

View File

@ -9,6 +9,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.automirrored.outlined.Label
import androidx.compose.material.icons.outlined.CloudOff import androidx.compose.material.icons.outlined.CloudOff
import androidx.compose.material.icons.outlined.GetApp import androidx.compose.material.icons.outlined.GetApp
import androidx.compose.material.icons.outlined.HelpOutline import androidx.compose.material.icons.outlined.HelpOutline
@ -128,7 +130,7 @@ fun MoreScreen(
item { item {
TextPreferenceWidget( TextPreferenceWidget(
title = stringResource(R.string.categories), title = stringResource(R.string.categories),
icon = Icons.Outlined.Label, icon = Icons.AutoMirrored.Outlined.Label,
onPreferenceClick = onClickCategories, onPreferenceClick = onClickCategories,
) )
} }
@ -166,7 +168,7 @@ fun MoreScreen(
item { item {
TextPreferenceWidget( TextPreferenceWidget(
title = stringResource(R.string.label_help), title = stringResource(R.string.label_help),
icon = Icons.Outlined.HelpOutline, icon = Icons.AutoMirrored.Outlined.HelpOutline,
onPreferenceClick = { uriHandler.openUri(Constants.URL_HELP) }, onPreferenceClick = { uriHandler.openUri(Constants.URL_HELP) },
) )
} }

View File

@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.OpenInNew
import androidx.compose.material.icons.filled.OpenInNew import androidx.compose.material.icons.filled.OpenInNew
import androidx.compose.material.icons.outlined.NewReleases import androidx.compose.material.icons.outlined.NewReleases
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -60,7 +61,7 @@ fun NewUpdateScreen(
) { ) {
Text(text = stringResource(R.string.update_check_open)) Text(text = stringResource(R.string.update_check_open))
Spacer(modifier = Modifier.width(MaterialTheme.padding.tiny)) Spacer(modifier = Modifier.width(MaterialTheme.padding.tiny))
Icon(imageVector = Icons.Default.OpenInNew, contentDescription = null) Icon(imageVector = Icons.AutoMirrored.Outlined.OpenInNew, contentDescription = null)
} }
} }
} }

View File

@ -2,12 +2,9 @@ package eu.kanade.presentation.more.settings
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.components.UpIcon import eu.kanade.presentation.components.AppBar
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
@Composable @Composable
@ -19,15 +16,9 @@ fun PreferenceScaffold(
) { ) {
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( AppBar(
title = { Text(text = stringResource(titleRes)) }, title = stringResource(titleRes),
navigationIcon = { navigateUp = onBackPressed,
if (onBackPressed != null) {
IconButton(onClick = onBackPressed) {
UpIcon()
}
}
},
actions = actions, actions = actions,
scrollBehavior = it, scrollBehavior = it,
) )

View File

@ -34,7 +34,6 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.download.DownloadCache import eu.kanade.tachiyomi.data.download.DownloadCache
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.NetworkPreferences import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.network.PREF_DOH_360 import eu.kanade.tachiyomi.network.PREF_DOH_360
@ -328,7 +327,6 @@ object SettingsAdvancedScreen : SearchableSettings {
private fun getLibraryGroup(): Preference.PreferenceGroup { private fun getLibraryGroup(): Preference.PreferenceGroup {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val context = LocalContext.current val context = LocalContext.current
val trackerManager = remember { Injekt.get<TrackerManager>() }
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(R.string.label_library), title = stringResource(R.string.label_library),
@ -337,12 +335,6 @@ object SettingsAdvancedScreen : SearchableSettings {
title = stringResource(R.string.pref_refresh_library_covers), title = stringResource(R.string.pref_refresh_library_covers),
onClick = { LibraryUpdateJob.startNow(context, target = LibraryUpdateJob.Target.COVERS) }, onClick = { LibraryUpdateJob.startNow(context, target = LibraryUpdateJob.Target.COVERS) },
), ),
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_refresh_library_tracking),
subtitle = stringResource(R.string.pref_refresh_library_tracking_summary),
enabled = trackerManager.hasLoggedIn(),
onClick = { LibraryUpdateJob.startNow(context, target = LibraryUpdateJob.Target.TRACKING) },
),
Preference.PreferenceItem.TextPreference( Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_reset_viewer_flags), title = stringResource(R.string.pref_reset_viewer_flags),
subtitle = stringResource(R.string.pref_reset_viewer_flags_summary), subtitle = stringResource(R.string.pref_reset_viewer_flags_summary),

View File

@ -9,20 +9,13 @@ import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -39,11 +32,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri import androidx.core.net.toUri
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.presentation.extensions.RequestStoragePermission
import eu.kanade.presentation.more.settings.Preference import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.permissions.PermissionRequestHelper
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupConst import eu.kanade.tachiyomi.data.backup.BackupConst
import eu.kanade.tachiyomi.data.backup.BackupCreateJob import eu.kanade.tachiyomi.data.backup.BackupCreateJob
@ -54,12 +46,12 @@ import eu.kanade.tachiyomi.data.sync.SyncDataJob
import eu.kanade.tachiyomi.data.sync.SyncManager import eu.kanade.tachiyomi.data.sync.SyncManager
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveService import eu.kanade.tachiyomi.data.sync.service.GoogleDriveService
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveSyncService import eu.kanade.tachiyomi.data.sync.service.GoogleDriveSyncService
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import tachiyomi.domain.backup.service.BackupPreferences import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.domain.sync.SyncPreferences import tachiyomi.domain.sync.SyncPreferences
import tachiyomi.presentation.core.components.ScrollbarLazyColumn import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.util.collectAsState import tachiyomi.presentation.core.util.collectAsState
@ -79,7 +71,7 @@ object SettingsBackupAndSyncScreen : SearchableSettings {
override fun getPreferences(): List<Preference> { override fun getPreferences(): List<Preference> {
val backupPreferences = Injekt.get<BackupPreferences>() val backupPreferences = Injekt.get<BackupPreferences>()
DiskUtil.RequestStoragePermission() PermissionRequestHelper.requestStoragePermission()
val syncPreferences = remember { Injekt.get<SyncPreferences>() } val syncPreferences = remember { Injekt.get<SyncPreferences>() }
val syncService by syncPreferences.syncService().collectAsState() val syncService by syncPreferences.syncService().collectAsState()
@ -263,22 +255,23 @@ object SettingsBackupAndSyncScreen : SearchableSettings {
val state = rememberLazyListState() val state = rememberLazyListState()
ScrollbarLazyColumn(state = state) { ScrollbarLazyColumn(state = state) {
item { item {
CreateBackupDialogItem( LabeledCheckbox(
isSelected = true, label = stringResource(R.string.manga),
title = stringResource(R.string.manga), checked = true,
onCheckedChange = {},
) )
} }
choices.forEach { (k, v) -> choices.forEach { (k, v) ->
item { item {
val isSelected = flags.contains(k) val isSelected = flags.contains(k)
CreateBackupDialogItem( LabeledCheckbox(
isSelected = isSelected, label = stringResource(v),
title = stringResource(v), checked = isSelected,
modifier = Modifier.clickable { onCheckedChange = {
if (isSelected) { if (it) {
flags.remove(k)
} else {
flags.add(k) flags.add(k)
} else {
flags.remove(k)
} }
}, },
) )
@ -307,29 +300,6 @@ object SettingsBackupAndSyncScreen : SearchableSettings {
) )
} }
@Composable
private fun CreateBackupDialogItem(
modifier: Modifier = Modifier,
isSelected: Boolean,
title: String,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier.fillMaxWidth(),
) {
Checkbox(
modifier = Modifier.heightIn(min = 48.dp),
checked = isSelected,
onCheckedChange = null,
)
Text(
text = title,
style = MaterialTheme.typography.bodyMedium.merge(),
modifier = Modifier.padding(start = 24.dp),
)
}
}
@Composable @Composable
private fun getRestoreBackupPref(): Preference.PreferenceItem.TextPreference { private fun getRestoreBackupPref(): Preference.PreferenceItem.TextPreference {
val context = LocalContext.current val context = LocalContext.current
@ -341,7 +311,7 @@ object SettingsBackupAndSyncScreen : SearchableSettings {
AlertDialog( AlertDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(R.string.invalid_backup_file)) }, title = { Text(text = stringResource(R.string.invalid_backup_file)) },
text = { Text(text = "${err.uri}\n\n${err.message}") }, text = { Text(text = listOfNotNull(err.uri, err.message).joinToString("\n\n")) },
dismissButton = { dismissButton = {
TextButton( TextButton(
onClick = { onClick = {
@ -349,7 +319,7 @@ object SettingsBackupAndSyncScreen : SearchableSettings {
onDismissRequest() onDismissRequest()
}, },
) { ) {
Text(text = stringResource(android.R.string.copy)) Text(text = stringResource(R.string.action_copy_to_clipboard))
} }
}, },
confirmButton = { confirmButton = {
@ -413,21 +383,24 @@ object SettingsBackupAndSyncScreen : SearchableSettings {
} }
}, },
) { ) {
if (it != null) { if (it == null) {
val results = try { error = InvalidRestore(message = context.getString(R.string.file_null_uri_error))
BackupFileValidator().validate(context, it) return@rememberLauncherForActivityResult
} catch (e: Exception) {
error = InvalidRestore(it, e.message.toString())
return@rememberLauncherForActivityResult
}
if (results.missingSources.isEmpty() && results.missingTrackers.isEmpty()) {
BackupRestoreJob.start(context, it)
return@rememberLauncherForActivityResult
}
error = MissingRestoreComponents(it, results.missingSources, results.missingTrackers)
} }
val results = try {
BackupFileValidator().validate(context, it)
} catch (e: Exception) {
error = InvalidRestore(it, e.message.toString())
return@rememberLauncherForActivityResult
}
if (results.missingSources.isEmpty() && results.missingTrackers.isEmpty()) {
BackupRestoreJob.start(context, it)
return@rememberLauncherForActivityResult
}
error = MissingRestoreComponents(it, results.missingSources, results.missingTrackers)
} }
return Preference.PreferenceItem.TextPreference( return Preference.PreferenceItem.TextPreference(
@ -649,6 +622,6 @@ private data class MissingRestoreComponents(
) )
private data class InvalidRestore( private data class InvalidRestore(
val uri: Uri, val uri: Uri? = null,
val message: String, val message: String,
) )

View File

@ -23,7 +23,6 @@ import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.widget.TriStateListDialog import eu.kanade.presentation.more.settings.widget.TriStateListDialog
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.ui.category.CategoryScreen import eu.kanade.tachiyomi.ui.category.CategoryScreen
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -197,12 +196,6 @@ object SettingsLibraryScreen : SearchableSettings {
title = stringResource(R.string.pref_library_update_refresh_metadata), title = stringResource(R.string.pref_library_update_refresh_metadata),
subtitle = stringResource(R.string.pref_library_update_refresh_metadata_summary), subtitle = stringResource(R.string.pref_library_update_refresh_metadata_summary),
), ),
Preference.PreferenceItem.SwitchPreference(
pref = libraryPreferences.autoUpdateTrackers(),
enabled = Injekt.get<TrackerManager>().hasLoggedIn(),
title = stringResource(R.string.pref_library_update_refresh_trackers),
subtitle = stringResource(R.string.pref_library_update_refresh_trackers_summary),
),
Preference.PreferenceItem.MultiSelectListPreference( Preference.PreferenceItem.MultiSelectListPreference(
pref = libraryPreferences.autoUpdateMangaRestrictions(), pref = libraryPreferences.autoUpdateMangaRestrictions(),
title = stringResource(R.string.pref_library_update_manga_restriction), title = stringResource(R.string.pref_library_update_manga_restriction),

View File

@ -9,6 +9,7 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ChromeReaderMode
import androidx.compose.material.icons.outlined.ChromeReaderMode import androidx.compose.material.icons.outlined.ChromeReaderMode
import androidx.compose.material.icons.outlined.Code import androidx.compose.material.icons.outlined.Code
import androidx.compose.material.icons.outlined.CollectionsBookmark import androidx.compose.material.icons.outlined.CollectionsBookmark
@ -20,11 +21,8 @@ import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.Security import androidx.compose.material.icons.outlined.Security
import androidx.compose.material.icons.outlined.SettingsBackupRestore import androidx.compose.material.icons.outlined.SettingsBackupRestore
import androidx.compose.material.icons.outlined.Sync import androidx.compose.material.icons.outlined.Sync
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -44,7 +42,6 @@ import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.UpIcon
import eu.kanade.presentation.more.settings.screen.about.AboutScreen import eu.kanade.presentation.more.settings.screen.about.AboutScreen
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
import eu.kanade.presentation.util.LocalBackPress import eu.kanade.presentation.util.LocalBackPress
@ -82,21 +79,13 @@ object SettingsMainScreen : Screen() {
val backPress = LocalBackPress.currentOrThrow val backPress = LocalBackPress.currentOrThrow
val containerColor = if (twoPane) getPalerSurface() else MaterialTheme.colorScheme.surface val containerColor = if (twoPane) getPalerSurface() else MaterialTheme.colorScheme.surface
val topBarState = rememberTopAppBarState() val topBarState = rememberTopAppBarState()
Scaffold( Scaffold(
topBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(topBarState), topBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(topBarState),
topBar = { scrollBehavior -> topBar = { scrollBehavior ->
TopAppBar( AppBar(
title = { title = stringResource(R.string.label_settings),
Text( navigateUp = backPress::invoke,
text = stringResource(R.string.label_settings),
modifier = Modifier.padding(start = 8.dp),
)
},
navigationIcon = {
IconButton(onClick = backPress::invoke) {
UpIcon()
}
},
actions = { actions = {
AppBarActions( AppBarActions(
listOf( listOf(
@ -198,7 +187,7 @@ object SettingsMainScreen : Screen() {
Item( Item(
titleRes = R.string.pref_category_reader, titleRes = R.string.pref_category_reader,
subtitleRes = R.string.pref_reader_summary, subtitleRes = R.string.pref_reader_summary,
icon = Icons.Outlined.ChromeReaderMode, icon = Icons.AutoMirrored.Outlined.ChromeReaderMode,
screen = SettingsReaderScreen, screen = SettingsReaderScreen,
), ),
Item( Item(

View File

@ -211,7 +211,10 @@ private fun SearchResult(
.toList() .toList()
} }
Crossfade(targetState = result) { Crossfade(
targetState = result,
label = "results",
) {
when { when {
it == null -> {} it == null -> {}
it.isEmpty() -> { it.isEmpty() -> {

View File

@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material.icons.outlined.Close import androidx.compose.material.icons.outlined.Close
@ -72,7 +73,7 @@ object SettingsTrackingScreen : SearchableSettings {
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
IconButton(onClick = { uriHandler.openUri("https://tachiyomi.org/docs/guides/tracking") }) { IconButton(onClick = { uriHandler.openUri("https://tachiyomi.org/docs/guides/tracking") }) {
Icon( Icon(
imageVector = Icons.Outlined.HelpOutline, imageVector = Icons.AutoMirrored.Outlined.HelpOutline,
contentDescription = stringResource(R.string.tracking_guide), contentDescription = stringResource(R.string.tracking_guide),
) )
} }

View File

@ -5,17 +5,11 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Public 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.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
@ -41,19 +35,9 @@ class OpenSourceLibraryLicenseScreen(
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( AppBar(
title = { title = name,
Text( navigateUp = navigator::pop,
text = name,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
navigationIcon = {
IconButton(onClick = navigator::pop) {
Icon(imageVector = Icons.Default.ArrowBack, contentDescription = null)
}
},
actions = { actions = {
if (!website.isNullOrEmpty()) { if (!website.isNullOrEmpty()) {
AppBarActions( AppBarActions(

View File

@ -31,8 +31,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastMap import androidx.compose.ui.util.fastMap
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope
import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.browse.components.SourceIcon import eu.kanade.presentation.browse.components.SourceIcon
@ -210,7 +210,7 @@ private class ClearDatabaseScreenModel : StateScreenModel<ClearDatabaseScreenMod
private val database: Database = Injekt.get() private val database: Database = Injekt.get()
init { init {
coroutineScope.launchIO { screenModelScope.launchIO {
getSourcesWithNonLibraryManga.subscribe() getSourcesWithNonLibraryManga.subscribe()
.collectLatest { list -> .collectLatest { list ->
mutableState.update { old -> mutableState.update { old ->

View File

@ -4,12 +4,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -43,13 +39,9 @@ class BackupSchemaScreen : Screen() {
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( AppBar(
title = { Text(text = title) }, title = title,
navigationIcon = { navigateUp = navigator::pop,
IconButton(onClick = navigator::pop) {
Icon(imageVector = Icons.Default.ArrowBack, contentDescription = null)
}
},
actions = { actions = {
AppBarActions( AppBarActions(
listOf( listOf(

View File

@ -1,7 +1,6 @@
package eu.kanade.presentation.more.settings.screen.debug package eu.kanade.presentation.more.settings.screen.debug
import android.os.Build import android.os.Build
import android.webkit.WebView
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -16,6 +15,7 @@ import eu.kanade.presentation.more.settings.screen.about.AboutScreen
import eu.kanade.presentation.util.Screen import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.WebViewUtil
import kotlinx.coroutines.guava.await import kotlinx.coroutines.guava.await
class DebugInfoScreen : Screen() { class DebugInfoScreen : Screen() {
@ -68,15 +68,7 @@ class DebugInfoScreen : Screen() {
@Composable @Composable
@ReadOnlyComposable @ReadOnlyComposable
private fun getWebViewVersion(): String { private fun getWebViewVersion(): String {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { return WebViewUtil.getVersion(LocalContext.current)
val webView = WebView.getCurrentWebViewPackage() ?: return "how did you get here?"
val pm = LocalContext.current.packageManager
val label = webView.applicationInfo.loadLabel(pm)
val version = webView.versionName
return "$label $version"
} else {
return "Unknown"
}
} }
@Composable @Composable

View File

@ -7,13 +7,9 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -61,13 +57,9 @@ class WorkerInfoScreen : Screen() {
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( AppBar(
title = { Text(text = title) }, title = title,
navigationIcon = { navigateUp = navigator::pop,
IconButton(onClick = navigator::pop) {
Icon(imageVector = Icons.Default.ArrowBack, contentDescription = null)
}
},
actions = { actions = {
AppBarActions( AppBarActions(
listOf( listOf(

View File

@ -1,30 +1,20 @@
package eu.kanade.presentation.more.settings.widget package eu.kanade.presentation.more.settings.widget
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.selection.selectable
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import eu.kanade.presentation.more.settings.Preference import eu.kanade.presentation.more.settings.Preference
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.components.LabeledCheckbox
@Composable @Composable
fun MultiSelectListPreferenceWidget( fun MultiSelectListPreferenceWidget(
@ -55,33 +45,17 @@ fun MultiSelectListPreferenceWidget(
preference.entries.forEach { current -> preference.entries.forEach { current ->
item { item {
val isSelected = selected.contains(current.key) val isSelected = selected.contains(current.key)
val onSelectionChanged = { LabeledCheckbox(
when (!isSelected) { label = current.value,
true -> selected.add(current.key) checked = isSelected,
false -> selected.remove(current.key) onCheckedChange = {
} if (it) {
} selected.add(current.key)
Row( } else {
verticalAlignment = Alignment.CenterVertically, selected.remove(current.key)
modifier = Modifier }
.clip(MaterialTheme.shapes.small) },
.selectable( )
selected = isSelected,
onClick = { onSelectionChanged() },
)
.minimumInteractiveComponentSize()
.fillMaxWidth(),
) {
Checkbox(
checked = isSelected,
onCheckedChange = null,
)
Text(
text = current.value,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(start = 24.dp),
)
}
} }
} }
} }

View File

@ -0,0 +1,20 @@
package eu.kanade.presentation.permissions
import android.Manifest
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import com.google.accompanist.permissions.rememberPermissionState
/**
* Launches request for [Manifest.permission.WRITE_EXTERNAL_STORAGE] permission
*/
object PermissionRequestHelper {
@Composable
fun requestStoragePermission() {
val permissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE)
LaunchedEffect(Unit) {
permissionState.launchPermissionRequest()
}
}
}

View File

@ -1,7 +1,6 @@
package eu.kanade.presentation.reader package eu.kanade.presentation.reader.appbars
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -10,11 +9,10 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -24,6 +22,7 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
@Composable @Composable
fun BottomReaderBar( fun BottomReaderBar(
backgroundColor: Color,
readingMode: ReadingModeType, readingMode: ReadingModeType,
onClickReadingMode: () -> Unit, onClickReadingMode: () -> Unit,
orientationMode: OrientationType, orientationMode: OrientationType,
@ -32,11 +31,6 @@ fun BottomReaderBar(
onClickCropBorder: () -> Unit, onClickCropBorder: () -> Unit,
onClickSettings: () -> Unit, onClickSettings: () -> Unit,
) { ) {
// Match with toolbar background color set in ReaderActivity
val backgroundColor = MaterialTheme.colorScheme
.surfaceColorAtElevation(3.dp)
.copy(alpha = if (isSystemInDarkTheme()) 0.9f else 0.95f)
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()

View File

@ -1,4 +1,4 @@
package eu.kanade.presentation.reader package eu.kanade.presentation.reader.appbars
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource

View File

@ -0,0 +1,166 @@
package eu.kanade.presentation.reader.appbars
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Bookmark
import androidx.compose.material.icons.outlined.BookmarkBorder
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.ui.reader.viewer.Viewer
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
private val animationSpec = tween<IntOffset>(200)
@Composable
fun ReaderAppBars(
visible: Boolean,
fullscreen: Boolean,
mangaTitle: String?,
chapterTitle: String?,
navigateUp: () -> Unit,
onClickTopAppBar: () -> Unit,
bookmarked: Boolean,
onToggleBookmarked: () -> Unit,
onOpenInWebView: (() -> Unit)?,
onShare: (() -> Unit)?,
viewer: Viewer?,
onNextChapter: () -> Unit,
enabledNext: Boolean,
onPreviousChapter: () -> Unit,
enabledPrevious: Boolean,
currentPage: Int,
totalPages: Int,
onSliderValueChange: (Int) -> Unit,
readingMode: ReadingModeType,
onClickReadingMode: () -> Unit,
orientationMode: OrientationType,
onClickOrientationMode: () -> Unit,
cropEnabled: Boolean,
onClickCropBorder: () -> Unit,
onClickSettings: () -> Unit,
) {
val isRtl = viewer is R2LPagerViewer
val backgroundColor = MaterialTheme.colorScheme
.surfaceColorAtElevation(3.dp)
.copy(alpha = if (isSystemInDarkTheme()) 0.9f else 0.95f)
val appBarModifier = if (fullscreen) {
Modifier.windowInsetsPadding(WindowInsets.systemBars)
} else {
Modifier
}
Column(
modifier = Modifier.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceBetween,
) {
AnimatedVisibility(
visible = visible,
enter = slideInVertically(
initialOffsetY = { -it },
animationSpec = animationSpec,
),
exit = slideOutVertically(
targetOffsetY = { -it },
animationSpec = animationSpec,
),
) {
AppBar(
modifier = appBarModifier
.clickable(onClick = onClickTopAppBar),
backgroundColor = backgroundColor,
title = mangaTitle,
subtitle = chapterTitle,
navigateUp = navigateUp,
actions = {
AppBarActions(
listOfNotNull(
AppBar.Action(
title = stringResource(if (bookmarked) R.string.action_remove_bookmark else R.string.action_bookmark),
icon = if (bookmarked) Icons.Outlined.Bookmark else Icons.Outlined.BookmarkBorder,
onClick = onToggleBookmarked,
),
onOpenInWebView?.let {
AppBar.OverflowAction(
title = stringResource(R.string.action_open_in_web_view),
onClick = it,
)
},
onShare?.let {
AppBar.OverflowAction(
title = stringResource(R.string.action_share),
onClick = it,
)
},
),
)
},
)
}
Spacer(modifier = Modifier.weight(1f))
AnimatedVisibility(
visible = visible,
enter = slideInVertically(
initialOffsetY = { it },
animationSpec = animationSpec,
),
exit = slideOutVertically(
targetOffsetY = { it },
animationSpec = animationSpec,
),
) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
ChapterNavigator(
isRtl = isRtl,
onNextChapter = onNextChapter,
enabledNext = enabledNext,
onPreviousChapter = onPreviousChapter,
enabledPrevious = enabledPrevious,
currentPage = currentPage,
totalPages = totalPages,
onSliderValueChange = onSliderValueChange,
)
BottomReaderBar(
backgroundColor = backgroundColor,
readingMode = readingMode,
onClickReadingMode = onClickReadingMode,
orientationMode = orientationMode,
onClickOrientationMode = onClickOrientationMode,
cropEnabled = cropEnabled,
onClickCropBorder = onClickCropBorder,
onClickSettings = onClickSettings,
)
}
}
}
}

View File

@ -44,14 +44,17 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.domain.track.model.toDbTrack import eu.kanade.domain.track.model.toDbTrack
import eu.kanade.presentation.components.DropdownMenu import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.presentation.track.components.TrackLogoIcon import eu.kanade.presentation.track.components.TrackLogoIcon
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.Tracker import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.ui.manga.track.TrackItem import eu.kanade.tachiyomi.ui.manga.track.TrackItem
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
import tachiyomi.presentation.core.util.ThemePreviews
import java.text.DateFormat import java.text.DateFormat
private const val UnsetStatusTextAlpha = 0.5F private const val UnsetStatusTextAlpha = 0.5F
@ -168,6 +171,7 @@ private fun TrackInfoItem(
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
) )
} }
VerticalDivider() VerticalDivider()
@ -254,6 +258,7 @@ private fun TrackDetailsItem(
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurface,
) )
} }
} }
@ -312,3 +317,12 @@ private fun TrackInfoItemMenu(
} }
} }
} }
@ThemePreviews
@Composable
private fun TrackInfoDialogHomePreviews(
@PreviewParameter(TrackInfoDialogHomePreviewProvider::class)
content: @Composable () -> Unit,
) {
TachiyomiTheme { content() }
}

View File

@ -0,0 +1,81 @@
package eu.kanade.presentation.track
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import eu.kanade.tachiyomi.dev.preview.DummyTracker
import eu.kanade.tachiyomi.ui.manga.track.TrackItem
import tachiyomi.domain.track.model.Track
import java.text.DateFormat
internal class TrackInfoDialogHomePreviewProvider :
PreviewParameterProvider<@Composable () -> Unit> {
private val aTrack = Track(
id = 1L,
mangaId = 2L,
syncId = 3L,
remoteId = 4L,
libraryId = null,
title = "Manage Name On Tracker Site",
lastChapterRead = 2.0,
totalChapters = 12L,
status = 1L,
score = 2.0,
remoteUrl = "https://example.com",
startDate = 0L,
finishDate = 0L,
)
private val trackItemWithoutTrack = TrackItem(
track = null,
tracker = DummyTracker(
id = 1L,
name = "Example Tracker",
),
)
private val trackItemWithTrack = TrackItem(
track = aTrack,
tracker = DummyTracker(
id = 2L,
name = "Example Tracker 2",
),
)
private val trackersWithAndWithoutTrack = @Composable {
TrackInfoDialogHome(
trackItems = listOf(
trackItemWithoutTrack,
trackItemWithTrack,
),
dateFormat = DateFormat.getDateInstance(),
onStatusClick = {},
onChapterClick = {},
onScoreClick = {},
onStartDateEdit = {},
onEndDateEdit = {},
onNewSearch = {},
onOpenInBrowser = {},
onRemoved = {},
)
}
private val noTrackers = @Composable {
TrackInfoDialogHome(
trackItems = listOf(),
dateFormat = DateFormat.getDateInstance(),
onStatusClick = {},
onChapterClick = {},
onScoreClick = {},
onStartDateEdit = {},
onEndDateEdit = {},
onNewSearch = {},
onOpenInBrowser = {},
onRemoved = {},
)
}
override val values: Sequence<@Composable () -> Unit>
get() = sequenceOf(
trackersWithAndWithoutTrack,
noTrackers,
)
}

View File

@ -30,12 +30,14 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.components.ScrollbarLazyColumn import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.components.WheelNumberPicker import tachiyomi.presentation.core.components.WheelNumberPicker
import tachiyomi.presentation.core.components.WheelTextPicker import tachiyomi.presentation.core.components.WheelTextPicker
import tachiyomi.presentation.core.components.material.AlertDialogContent import tachiyomi.presentation.core.components.material.AlertDialogContent
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.util.ThemePreviews
import tachiyomi.presentation.core.util.isScrolledToEnd import tachiyomi.presentation.core.util.isScrolledToEnd
import tachiyomi.presentation.core.util.isScrolledToStart import tachiyomi.presentation.core.util.isScrolledToStart
@ -171,7 +173,7 @@ fun TrackDateSelector(
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
} }
TextButton(onClick = onDismissRequest) { TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel)) Text(text = stringResource(R.string.action_cancel))
} }
TextButton(onClick = { onConfirm(pickerState.selectedDateMillis!!) }) { TextButton(onClick = { onConfirm(pickerState.selectedDateMillis!!) }) {
Text(text = stringResource(R.string.action_ok)) Text(text = stringResource(R.string.action_ok))
@ -209,7 +211,7 @@ private fun BaseSelector(
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
} }
TextButton(onClick = onDismissRequest) { TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel)) Text(text = stringResource(R.string.action_cancel))
} }
TextButton(onClick = onConfirm) { TextButton(onClick = onConfirm) {
Text(text = stringResource(R.string.action_ok)) Text(text = stringResource(R.string.action_ok))
@ -218,3 +220,25 @@ private fun BaseSelector(
}, },
) )
} }
@ThemePreviews
@Composable
private fun TrackStatusSelectorPreviews() {
TachiyomiTheme {
TrackStatusSelector(
selection = 1,
onSelectionChange = {},
selections = mapOf(
// Anilist values
1 to R.string.reading,
2 to R.string.plan_to_read,
3 to R.string.completed,
4 to R.string.on_hold,
5 to R.string.dropped,
6 to R.string.repeating,
),
onConfirm = {},
onDismissRequest = {},
)
}
}

View File

@ -28,6 +28,7 @@ import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
@ -56,8 +57,10 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.toLowerCase import androidx.compose.ui.text.toLowerCase
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.presentation.manga.components.MangaCover import eu.kanade.presentation.manga.components.MangaCover
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import tachiyomi.presentation.core.components.ScrollbarLazyColumn import tachiyomi.presentation.core.components.ScrollbarLazyColumn
@ -65,6 +68,7 @@ import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen import tachiyomi.presentation.core.screens.LoadingScreen
import tachiyomi.presentation.core.util.ThemePreviews
import tachiyomi.presentation.core.util.plus import tachiyomi.presentation.core.util.plus
import tachiyomi.presentation.core.util.runOnEnterKeyPressed import tachiyomi.presentation.core.util.runOnEnterKeyPressed
import tachiyomi.presentation.core.util.secondaryItemAlpha import tachiyomi.presentation.core.util.secondaryItemAlpha
@ -94,7 +98,7 @@ fun TrackerSearch(
navigationIcon = { navigationIcon = {
IconButton(onClick = onDismissRequest) { IconButton(onClick = onDismissRequest) {
Icon( Icon(
imageVector = Icons.Default.ArrowBack, imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant, tint = MaterialTheme.colorScheme.onSurfaceVariant,
) )
@ -315,3 +319,12 @@ private fun SearchResultItemDetails(
) )
} }
} }
@ThemePreviews
@Composable
private fun TrackerSearchPreviews(
@PreviewParameter(TrackerSearchPreviewProvider::class)
content: @Composable () -> Unit,
) {
TachiyomiTheme { content() }
}

View File

@ -0,0 +1,84 @@
package eu.kanade.presentation.track
import androidx.compose.runtime.Composable
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import java.time.Instant
import java.time.temporal.ChronoUnit
import kotlin.random.Random
internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composable () -> Unit> {
private val fullPageWithSecondSelected = @Composable {
val items = someTrackSearches().take(30).toList()
TrackerSearch(
query = TextFieldValue(text = "search text"),
onQueryChange = {},
onDispatchQuery = {},
queryResult = Result.success(items),
selected = items[1],
onSelectedChange = {},
onConfirmSelection = {},
onDismissRequest = {},
)
}
private val fullPageWithoutSelected = @Composable {
TrackerSearch(
query = TextFieldValue(text = ""),
onQueryChange = {},
onDispatchQuery = {},
queryResult = Result.success(someTrackSearches().take(30).toList()),
selected = null,
onSelectedChange = {},
onConfirmSelection = {},
onDismissRequest = {},
)
}
private val loading = @Composable {
TrackerSearch(
query = TextFieldValue(),
onQueryChange = {},
onDispatchQuery = {},
queryResult = null,
selected = null,
onSelectedChange = {},
onConfirmSelection = {},
onDismissRequest = {},
)
}
override val values: Sequence<@Composable () -> Unit> = sequenceOf(
fullPageWithSecondSelected,
fullPageWithoutSelected,
loading,
)
private fun someTrackSearches(): Sequence<TrackSearch> = sequence {
while (true) {
yield(randTrackSearch())
}
}
private fun randTrackSearch() = TrackSearch().let {
it.id = Random.nextLong()
it.manga_id = Random.nextLong()
it.sync_id = Random.nextInt()
it.media_id = Random.nextLong()
it.library_id = Random.nextLong()
it.title = lorem((1..10).random()).joinToString()
it.last_chapter_read = (0..100).random().toFloat()
it.total_chapters = (100..1000).random()
it.score = (0..10).random().toFloat()
it.status = Random.nextInt()
it.started_reading_date = 0L
it.finished_reading_date = 0L
it.tracking_url = "https://example.com/tracker-example"
it.cover_url = "https://example.com/cover.png"
it.start_date = Instant.now().minus((1L..365).random(), ChronoUnit.DAYS).toString()
it.summary = lorem((0..40).random()).joinToString()
it
}
private fun lorem(words: Int): Sequence<String> =
LoremIpsum(words).values
}

View File

@ -11,8 +11,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.data.track.Tracker import eu.kanade.tachiyomi.data.track.Tracker
import tachiyomi.presentation.core.util.ThemePreviews
import tachiyomi.presentation.core.util.clickableNoIndication import tachiyomi.presentation.core.util.clickableNoIndication
@Composable @Composable
@ -39,3 +42,17 @@ fun TrackLogoIcon(
) )
} }
} }
@ThemePreviews
@Composable
private fun TrackLogoIconPreviews(
@PreviewParameter(TrackLogoIconPreviewProvider::class)
tracker: Tracker,
) {
TachiyomiTheme {
TrackLogoIcon(
tracker = tracker,
onClick = null,
)
}
}

View File

@ -0,0 +1,20 @@
package eu.kanade.presentation.track.components
import android.graphics.Color
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.dev.preview.DummyTracker
internal class TrackLogoIconPreviewProvider : PreviewParameterProvider<Tracker> {
override val values: Sequence<Tracker>
get() = sequenceOf(
DummyTracker(
id = 1L,
name = "Dummy Tracker",
valLogoColor = Color.rgb(18, 25, 35),
valLogo = R.drawable.ic_tracker_anilist,
),
)
}

View File

@ -7,13 +7,18 @@ import android.webkit.WebView
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
import androidx.compose.material.icons.outlined.ArrowBack import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material.icons.outlined.ArrowForward import androidx.compose.material.icons.outlined.ArrowForward
import androidx.compose.material.icons.outlined.Close import androidx.compose.material.icons.outlined.Close
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -22,8 +27,10 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.google.accompanist.web.AccompanistWebViewClient import com.google.accompanist.web.AccompanistWebViewClient
import com.google.accompanist.web.LoadingState import com.google.accompanist.web.LoadingState
import com.google.accompanist.web.WebView import com.google.accompanist.web.WebView
@ -72,7 +79,7 @@ fun WebViewScreenContent(
super.onPageFinished(view, url) super.onPageFinished(view, url)
scope.launch { scope.launch {
val html = view.getHtml() val html = view.getHtml()
showCloudflareHelp = "window._cf_chl_opt" in html showCloudflareHelp = "window._cf_chl_opt" in html || "Ray ID is" in html
} }
} }
@ -103,54 +110,71 @@ fun WebViewScreenContent(
Scaffold( Scaffold(
topBar = { topBar = {
Box { Box {
AppBar( Column {
title = state.pageTitle ?: initialTitle, AppBar(
subtitle = currentUrl, title = state.pageTitle ?: initialTitle,
navigateUp = onNavigateUp, subtitle = currentUrl,
navigationIcon = Icons.Outlined.Close, navigateUp = onNavigateUp,
actions = { navigationIcon = Icons.Outlined.Close,
AppBarActions( actions = {
listOf( AppBarActions(
AppBar.Action( listOf(
title = stringResource(R.string.action_webview_back), AppBar.Action(
icon = Icons.Outlined.ArrowBack, title = stringResource(R.string.action_webview_back),
onClick = { icon = Icons.AutoMirrored.Outlined.ArrowBack,
if (navigator.canGoBack) { onClick = {
navigator.navigateBack() if (navigator.canGoBack) {
} navigator.navigateBack()
}
},
enabled = navigator.canGoBack,
),
AppBar.Action(
title = stringResource(R.string.action_webview_forward),
icon = Icons.AutoMirrored.Outlined.ArrowForward,
onClick = {
if (navigator.canGoForward) {
navigator.navigateForward()
}
},
enabled = navigator.canGoForward,
),
AppBar.OverflowAction(
title = stringResource(R.string.action_webview_refresh),
onClick = { navigator.reload() },
),
AppBar.OverflowAction(
title = stringResource(R.string.action_share),
onClick = { onShare(currentUrl) },
),
AppBar.OverflowAction(
title = stringResource(R.string.action_open_in_browser),
onClick = { onOpenInBrowser(currentUrl) },
),
AppBar.OverflowAction(
title = stringResource(R.string.pref_clear_cookies),
onClick = { onClearCookies(currentUrl) },
),
),
)
},
)
if (showCloudflareHelp) {
Surface(
modifier = Modifier.padding(8.dp),
) {
WarningBanner(
textRes = R.string.information_cloudflare_help,
modifier = Modifier
.clip(MaterialTheme.shapes.small)
.clickable {
uriHandler.openUri("https://tachiyomi.org/docs/guides/troubleshooting/#cloudflare")
}, },
enabled = navigator.canGoBack, )
), }
AppBar.Action( }
title = stringResource(R.string.action_webview_forward), }
icon = Icons.Outlined.ArrowForward,
onClick = {
if (navigator.canGoForward) {
navigator.navigateForward()
}
},
enabled = navigator.canGoForward,
),
AppBar.OverflowAction(
title = stringResource(R.string.action_webview_refresh),
onClick = { navigator.reload() },
),
AppBar.OverflowAction(
title = stringResource(R.string.action_share),
onClick = { onShare(currentUrl) },
),
AppBar.OverflowAction(
title = stringResource(R.string.action_open_in_browser),
onClick = { onOpenInBrowser(currentUrl) },
),
AppBar.OverflowAction(
title = stringResource(R.string.pref_clear_cookies),
onClick = { onClearCookies(currentUrl) },
),
),
)
},
)
when (val loadingState = state.loadingState) { when (val loadingState = state.loadingState) {
is LoadingState.Initializing -> LinearProgressIndicator( is LoadingState.Initializing -> LinearProgressIndicator(
modifier = Modifier modifier = Modifier
@ -158,7 +182,7 @@ fun WebViewScreenContent(
.align(Alignment.BottomCenter), .align(Alignment.BottomCenter),
) )
is LoadingState.Loading -> LinearProgressIndicator( is LoadingState.Loading -> LinearProgressIndicator(
progress = (loadingState as? LoadingState.Loading)?.progress ?: 1f, progress = { (loadingState as? LoadingState.Loading)?.progress ?: 1f },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.align(Alignment.BottomCenter), .align(Alignment.BottomCenter),
@ -168,38 +192,27 @@ fun WebViewScreenContent(
} }
}, },
) { contentPadding -> ) { contentPadding ->
Column( WebView(
modifier = Modifier.padding(contentPadding), state = state,
) { modifier = Modifier
if (showCloudflareHelp) { .fillMaxSize()
WarningBanner( .padding(contentPadding),
textRes = R.string.information_cloudflare_help, navigator = navigator,
modifier = Modifier.clickable { onCreated = { webView ->
uriHandler.openUri("https://tachiyomi.org/docs/guides/troubleshooting/#cloudflare") webView.setDefaultSettings()
},
)
}
WebView( // Debug mode (chrome://inspect/#devices)
state = state, if (BuildConfig.DEBUG &&
modifier = Modifier.weight(1f), 0 != webView.context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE
navigator = navigator, ) {
onCreated = { webView -> WebView.setWebContentsDebuggingEnabled(true)
webView.setDefaultSettings() }
// Debug mode (chrome://inspect/#devices) headers["user-agent"]?.let {
if (BuildConfig.DEBUG && webView.settings.userAgentString = it
0 != webView.context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE }
) { },
WebView.setWebContentsDebuggingEnabled(true) client = webClient,
} )
headers["user-agent"]?.let {
webView.settings.userAgentString = it
}
},
client = webClient,
)
}
} }
} }

View File

@ -238,7 +238,7 @@ class BackupCreator(
fun backupSourcePreferences(flags: Int): List<BackupSourcePreferences> { fun backupSourcePreferences(flags: Int): List<BackupSourcePreferences> {
if (flags and BACKUP_SOURCE_PREFS_MASK != BACKUP_SOURCE_PREFS) return emptyList() if (flags and BACKUP_SOURCE_PREFS_MASK != BACKUP_SOURCE_PREFS) return emptyList()
return sourceManager.getOnlineSources() return sourceManager.getCatalogueSources()
.filterIsInstance<ConfigurableSource>() .filterIsInstance<ConfigurableSource>()
.map { .map {
BackupSourcePreferences( BackupSourcePreferences(

View File

@ -17,6 +17,7 @@ import eu.kanade.tachiyomi.data.backup.models.IntPreferenceValue
import eu.kanade.tachiyomi.data.backup.models.LongPreferenceValue import eu.kanade.tachiyomi.data.backup.models.LongPreferenceValue
import eu.kanade.tachiyomi.data.backup.models.StringPreferenceValue import eu.kanade.tachiyomi.data.backup.models.StringPreferenceValue
import eu.kanade.tachiyomi.data.backup.models.StringSetPreferenceValue import eu.kanade.tachiyomi.data.backup.models.StringSetPreferenceValue
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.source.model.copyFrom import eu.kanade.tachiyomi.source.model.copyFrom
import eu.kanade.tachiyomi.source.sourcePreferences import eu.kanade.tachiyomi.source.sourcePreferences
import eu.kanade.tachiyomi.util.BackupUtil import eu.kanade.tachiyomi.util.BackupUtil
@ -589,6 +590,9 @@ class BackupRestorer(
private fun restoreAppPreferences(preferences: List<BackupPreference>) { private fun restoreAppPreferences(preferences: List<BackupPreference>) {
restorePreferences(preferences, preferenceStore) restorePreferences(preferences, preferenceStore)
LibraryUpdateJob.setupTask(context)
BackupCreateJob.setupTask(context)
restoreProgress += 1 restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.app_settings), context.getString(R.string.restoring_backup)) showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.app_settings), context.getString(R.string.restoring_backup))
} }

View File

@ -18,7 +18,6 @@ import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.copyFrom import eu.kanade.domain.manga.model.copyFrom
import eu.kanade.domain.manga.model.toSManga import eu.kanade.domain.manga.model.toSManga
import eu.kanade.domain.track.interactor.RefreshTracks
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
@ -89,7 +88,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
private val updateManga: UpdateManga = Injekt.get() private val updateManga: UpdateManga = Injekt.get()
private val getCategories: GetCategories = Injekt.get() private val getCategories: GetCategories = Injekt.get()
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get() private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get()
private val refreshTracks: RefreshTracks = Injekt.get()
private val fetchInterval: FetchInterval = Injekt.get() private val fetchInterval: FetchInterval = Injekt.get()
private val notifier = LibraryUpdateNotifier(context) private val notifier = LibraryUpdateNotifier(context)
@ -131,7 +129,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
when (target) { when (target) {
Target.CHAPTERS -> updateChapterList() Target.CHAPTERS -> updateChapterList()
Target.COVERS -> updateCovers() Target.COVERS -> updateCovers()
Target.TRACKING -> updateTrackings()
} }
Result.success() Result.success()
} catch (e: Exception) { } catch (e: Exception) {
@ -304,10 +301,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
} }
failedUpdates.add(manga to errorMessage) failedUpdates.add(manga to errorMessage)
} }
if (libraryPreferences.autoUpdateTrackers().get()) {
refreshTracks(manga.id)
}
} }
} }
} }
@ -409,33 +402,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
notifier.cancelProgressNotification() notifier.cancelProgressNotification()
} }
/**
* Method that updates the metadata of the connected tracking services. It's called in a
* background thread, so it's safe to do heavy operations or network calls here.
*/
private suspend fun updateTrackings() {
coroutineScope {
var progressCount = 0
mangaToUpdate.forEach { libraryManga ->
ensureActive()
val manga = libraryManga.manga
notifier.showProgressNotification(listOf(manga), progressCount++, mangaToUpdate.size)
refreshTracks(manga.id)
}
notifier.cancelProgressNotification()
}
}
private suspend fun refreshTracks(mangaId: Long) {
refreshTracks.await(mangaId).forEach { (_, e) ->
// Ignore errors and continue
logcat(LogPriority.ERROR, e)
}
}
private suspend fun withUpdateNotification( private suspend fun withUpdateNotification(
updatingManga: CopyOnWriteArrayList<Manga>, updatingManga: CopyOnWriteArrayList<Manga>,
completed: AtomicInteger, completed: AtomicInteger,
@ -500,7 +466,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
enum class Target { enum class Target {
CHAPTERS, // Manga chapters CHAPTERS, // Manga chapters
COVERS, // Manga covers COVERS, // Manga covers
TRACKING, // Tracking metadata
} }
companion object { companion object {

View File

@ -0,0 +1,169 @@
package eu.kanade.tachiyomi.data.track
import android.app.Application
import androidx.annotation.CallSuper
import eu.kanade.domain.track.interactor.SyncChapterProgressWithTrack
import eu.kanade.domain.track.model.toDbTrack
import eu.kanade.domain.track.model.toDomainTrack
import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone
import eu.kanade.tachiyomi.util.system.toast
import logcat.LogPriority
import okhttp3.OkHttpClient
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.chapter.interactor.GetChapterByMangaId
import tachiyomi.domain.history.interactor.GetHistory
import tachiyomi.domain.track.interactor.InsertTrack
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.time.ZoneOffset
import tachiyomi.domain.track.model.Track as DomainTrack
abstract class BaseTracker(
override val id: Long,
override val name: String,
) : Tracker {
val trackPreferences: TrackPreferences by injectLazy()
val networkService: NetworkHelper by injectLazy()
private val insertTrack: InsertTrack by injectLazy()
private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack by injectLazy()
override val client: OkHttpClient
get() = networkService.client
// Application and remote support for reading dates
override val supportsReadingDates: Boolean = false
// TODO: Store all scores as 10 point in the future maybe?
override fun get10PointScore(track: DomainTrack): Double {
return track.score
}
override fun indexToScore(index: Int): Float {
return index.toFloat()
}
@CallSuper
override fun logout() {
trackPreferences.setCredentials(this, "", "")
}
override val isLoggedIn: Boolean
get() = getUsername().isNotEmpty() &&
getPassword().isNotEmpty()
override fun getUsername() = trackPreferences.trackUsername(this).get()
override fun getPassword() = trackPreferences.trackPassword(this).get()
override fun saveCredentials(username: String, password: String) {
trackPreferences.setCredentials(this, username, password)
}
// TODO: move this to an interactor, and update all trackers based on common data
override suspend fun register(item: Track, mangaId: Long) {
item.manga_id = mangaId
try {
withIOContext {
val allChapters = Injekt.get<GetChapterByMangaId>().await(mangaId)
val hasReadChapters = allChapters.any { it.read }
bind(item, hasReadChapters)
var track = item.toDomainTrack(idRequired = false) ?: return@withIOContext
insertTrack.await(track)
// TODO: merge into [SyncChapterProgressWithTrack]?
// Update chapter progress if newer chapters marked read locally
if (hasReadChapters) {
val latestLocalReadChapterNumber = allChapters
.sortedBy { it.chapterNumber }
.takeWhile { it.read }
.lastOrNull()
?.chapterNumber ?: -1.0
if (latestLocalReadChapterNumber > track.lastChapterRead) {
track = track.copy(
lastChapterRead = latestLocalReadChapterNumber,
)
setRemoteLastChapterRead(track.toDbTrack(), latestLocalReadChapterNumber.toInt())
}
if (track.startDate <= 0) {
val firstReadChapterDate = Injekt.get<GetHistory>().await(mangaId)
.sortedBy { it.readAt }
.firstOrNull()
?.readAt
firstReadChapterDate?.let {
val startDate = firstReadChapterDate.time.convertEpochMillisZone(ZoneOffset.systemDefault(), ZoneOffset.UTC)
track = track.copy(
startDate = startDate,
)
setRemoteStartDate(track.toDbTrack(), startDate)
}
}
}
syncChapterProgressWithTrack.await(mangaId, track, this@BaseTracker)
}
} catch (e: Throwable) {
withUIContext { Injekt.get<Application>().toast(e.message) }
}
}
override suspend fun setRemoteStatus(track: Track, status: Int) {
track.status = status
if (track.status == getCompletionStatus() && track.total_chapters != 0) {
track.last_chapter_read = track.total_chapters.toFloat()
}
withIOContext { updateRemote(track) }
}
override suspend fun setRemoteLastChapterRead(track: Track, chapterNumber: Int) {
if (track.last_chapter_read == 0f && track.last_chapter_read < chapterNumber && track.status != getRereadingStatus()) {
track.status = getReadingStatus()
}
track.last_chapter_read = chapterNumber.toFloat()
if (track.total_chapters != 0 && track.last_chapter_read.toInt() == track.total_chapters) {
track.status = getCompletionStatus()
track.finished_reading_date = System.currentTimeMillis()
}
withIOContext { updateRemote(track) }
}
override suspend fun setRemoteScore(track: Track, scoreString: String) {
track.score = indexToScore(getScoreList().indexOf(scoreString))
withIOContext { updateRemote(track) }
}
override suspend fun setRemoteStartDate(track: Track, epochMillis: Long) {
track.started_reading_date = epochMillis
withIOContext { updateRemote(track) }
}
override suspend fun setRemoteFinishDate(track: Track, epochMillis: Long) {
track.finished_reading_date = epochMillis
withIOContext { updateRemote(track) }
}
private suspend fun updateRemote(track: Track) {
withIOContext {
try {
update(track)
track.toDomainTrack(idRequired = false)?.let {
insertTrack.await(it)
}
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to update remote track data id=$id" }
withUIContext { Injekt.get<Application>().toast(e.message) }
}
}
}
}

View File

@ -1,201 +1,81 @@
package eu.kanade.tachiyomi.data.track package eu.kanade.tachiyomi.data.track
import android.app.Application
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import eu.kanade.domain.track.interactor.SyncChapterProgressWithTrack
import eu.kanade.domain.track.model.toDbTrack
import eu.kanade.domain.track.model.toDomainTrack
import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone
import eu.kanade.tachiyomi.util.system.toast
import logcat.LogPriority
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.chapter.interactor.GetChapterByMangaId
import tachiyomi.domain.history.interactor.GetHistory
import tachiyomi.domain.track.interactor.InsertTrack
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.time.ZoneOffset
import tachiyomi.domain.track.model.Track as DomainTrack
abstract class Tracker(val id: Long, val name: String) { interface Tracker {
val trackPreferences: TrackPreferences by injectLazy() val id: Long
val networkService: NetworkHelper by injectLazy()
private val insertTrack: InsertTrack by injectLazy()
private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack by injectLazy()
open val client: OkHttpClient val name: String
get() = networkService.client
val client: OkHttpClient
// Application and remote support for reading dates // Application and remote support for reading dates
open val supportsReadingDates: Boolean = false val supportsReadingDates: Boolean
@DrawableRes
abstract fun getLogo(): Int
@ColorInt @ColorInt
abstract fun getLogoColor(): Int fun getLogoColor(): Int
abstract fun getStatusList(): List<Int> @DrawableRes
fun getLogo(): Int
fun getStatusList(): List<Int>
@StringRes @StringRes
abstract fun getStatus(status: Int): Int? fun getStatus(status: Int): Int?
abstract fun getReadingStatus(): Int fun getReadingStatus(): Int
abstract fun getRereadingStatus(): Int fun getRereadingStatus(): Int
abstract fun getCompletionStatus(): Int fun getCompletionStatus(): Int
abstract fun getScoreList(): List<String> fun getScoreList(): List<String>
// TODO: Store all scores as 10 point in the future maybe? // TODO: Store all scores as 10 point in the future maybe?
open fun get10PointScore(track: DomainTrack): Double { fun get10PointScore(track: tachiyomi.domain.track.model.Track): Double
return track.score
}
open fun indexToScore(index: Int): Float { fun indexToScore(index: Int): Float
return index.toFloat()
}
abstract fun displayScore(track: Track): String fun displayScore(track: Track): String
abstract suspend fun update(track: Track, didReadChapter: Boolean = false): Track suspend fun update(track: Track, didReadChapter: Boolean = false): Track
abstract suspend fun bind(track: Track, hasReadChapters: Boolean = false): Track suspend fun bind(track: Track, hasReadChapters: Boolean = false): Track
abstract suspend fun search(query: String): List<TrackSearch> suspend fun search(query: String): List<TrackSearch>
abstract suspend fun refresh(track: Track): Track suspend fun refresh(track: Track): Track
abstract suspend fun login(username: String, password: String) suspend fun login(username: String, password: String)
@CallSuper @CallSuper
open fun logout() { fun logout()
trackPreferences.setCredentials(this, "", "")
}
open val isLoggedIn: Boolean val isLoggedIn: Boolean
get() = getUsername().isNotEmpty() &&
getPassword().isNotEmpty()
fun getUsername() = trackPreferences.trackUsername(this).get() fun getUsername(): String
fun getPassword() = trackPreferences.trackPassword(this).get() fun getPassword(): String
fun saveCredentials(username: String, password: String) { fun saveCredentials(username: String, password: String)
trackPreferences.setCredentials(this, username, password)
}
// TODO: move this to an interactor, and update all trackers based on common data // TODO: move this to an interactor, and update all trackers based on common data
suspend fun register(item: Track, mangaId: Long) { suspend fun register(item: Track, mangaId: Long)
item.manga_id = mangaId
try {
withIOContext {
val allChapters = Injekt.get<GetChapterByMangaId>().await(mangaId)
val hasReadChapters = allChapters.any { it.read }
bind(item, hasReadChapters)
var track = item.toDomainTrack(idRequired = false) ?: return@withIOContext suspend fun setRemoteStatus(track: Track, status: Int)
insertTrack.await(track) suspend fun setRemoteLastChapterRead(track: Track, chapterNumber: Int)
// TODO: merge into [SyncChapterProgressWithTrack]? suspend fun setRemoteScore(track: Track, scoreString: String)
// Update chapter progress if newer chapters marked read locally
if (hasReadChapters) {
val latestLocalReadChapterNumber = allChapters
.sortedBy { it.chapterNumber }
.takeWhile { it.read }
.lastOrNull()
?.chapterNumber ?: -1.0
if (latestLocalReadChapterNumber > track.lastChapterRead) { suspend fun setRemoteStartDate(track: Track, epochMillis: Long)
track = track.copy(
lastChapterRead = latestLocalReadChapterNumber,
)
setRemoteLastChapterRead(track.toDbTrack(), latestLocalReadChapterNumber.toInt())
}
if (track.startDate <= 0) { suspend fun setRemoteFinishDate(track: Track, epochMillis: Long)
val firstReadChapterDate = Injekt.get<GetHistory>().await(mangaId)
.sortedBy { it.readAt }
.firstOrNull()
?.readAt
firstReadChapterDate?.let {
val startDate = firstReadChapterDate.time.convertEpochMillisZone(ZoneOffset.systemDefault(), ZoneOffset.UTC)
track = track.copy(
startDate = startDate,
)
setRemoteStartDate(track.toDbTrack(), startDate)
}
}
}
syncChapterProgressWithTrack.await(mangaId, track, this@Tracker)
}
} catch (e: Throwable) {
withUIContext { Injekt.get<Application>().toast(e.message) }
}
}
suspend fun setRemoteStatus(track: Track, status: Int) {
track.status = status
if (track.status == getCompletionStatus() && track.total_chapters != 0) {
track.last_chapter_read = track.total_chapters.toFloat()
}
withIOContext { updateRemote(track) }
}
suspend fun setRemoteLastChapterRead(track: Track, chapterNumber: Int) {
if (track.last_chapter_read == 0f && track.last_chapter_read < chapterNumber && track.status != getRereadingStatus()) {
track.status = getReadingStatus()
}
track.last_chapter_read = chapterNumber.toFloat()
if (track.total_chapters != 0 && track.last_chapter_read.toInt() == track.total_chapters) {
track.status = getCompletionStatus()
track.finished_reading_date = System.currentTimeMillis()
}
withIOContext { updateRemote(track) }
}
suspend fun setRemoteScore(track: Track, scoreString: String) {
track.score = indexToScore(getScoreList().indexOf(scoreString))
withIOContext { updateRemote(track) }
}
suspend fun setRemoteStartDate(track: Track, epochMillis: Long) {
track.started_reading_date = epochMillis
withIOContext { updateRemote(track) }
}
suspend fun setRemoteFinishDate(track: Track, epochMillis: Long) {
track.finished_reading_date = epochMillis
withIOContext { updateRemote(track) }
}
private suspend fun updateRemote(track: Track) {
withIOContext {
try {
update(track)
track.toDomainTrack(idRequired = false)?.let {
insertTrack.await(it)
}
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to update remote track data id=$id" }
withUIContext { Injekt.get<Application>().toast(e.message) }
}
}
}
} }

View File

@ -4,15 +4,15 @@ import android.graphics.Color
import androidx.annotation.StringRes import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.BaseTracker
import eu.kanade.tachiyomi.data.track.DeletableTracker import eu.kanade.tachiyomi.data.track.DeletableTracker
import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import tachiyomi.domain.track.model.Track as DomainTrack import tachiyomi.domain.track.model.Track as DomainTrack
class Anilist(id: Long) : Tracker(id, "AniList"), DeletableTracker { class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
companion object { companion object {
const val READING = 1 const val READING = 1

View File

@ -4,13 +4,13 @@ import android.graphics.Color
import androidx.annotation.StringRes import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.Tracker import eu.kanade.tachiyomi.data.track.BaseTracker
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
class Bangumi(id: Long) : Tracker(id, "Bangumi") { class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
private val json: Json by injectLazy() private val json: Json by injectLazy()

View File

@ -4,8 +4,8 @@ import android.graphics.Color
import androidx.annotation.StringRes import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.BaseTracker
import eu.kanade.tachiyomi.data.track.EnhancedTracker import eu.kanade.tachiyomi.data.track.EnhancedTracker
import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
@ -16,7 +16,7 @@ import uy.kohesive.injekt.injectLazy
import java.security.MessageDigest import java.security.MessageDigest
import tachiyomi.domain.track.model.Track as DomainTrack import tachiyomi.domain.track.model.Track as DomainTrack
class Kavita(id: Long) : Tracker(id, "Kavita"), EnhancedTracker { class Kavita(id: Long) : BaseTracker(id, "Kavita"), EnhancedTracker {
companion object { companion object {
const val UNREAD = 1 const val UNREAD = 1

View File

@ -4,15 +4,15 @@ import android.graphics.Color
import androidx.annotation.StringRes import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.BaseTracker
import eu.kanade.tachiyomi.data.track.DeletableTracker import eu.kanade.tachiyomi.data.track.DeletableTracker
import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.text.DecimalFormat import java.text.DecimalFormat
class Kitsu(id: Long) : Tracker(id, "Kitsu"), DeletableTracker { class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
companion object { companion object {
const val READING = 1 const val READING = 1

View File

@ -4,8 +4,8 @@ import android.graphics.Color
import androidx.annotation.StringRes import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.BaseTracker
import eu.kanade.tachiyomi.data.track.EnhancedTracker import eu.kanade.tachiyomi.data.track.EnhancedTracker
import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import okhttp3.Dns import okhttp3.Dns
@ -13,7 +13,7 @@ import okhttp3.OkHttpClient
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.track.model.Track as DomainTrack import tachiyomi.domain.track.model.Track as DomainTrack
class Komga(id: Long) : Tracker(id, "Komga"), EnhancedTracker { class Komga(id: Long) : BaseTracker(id, "Komga"), EnhancedTracker {
companion object { companion object {
const val UNREAD = 1 const val UNREAD = 1

View File

@ -4,13 +4,13 @@ import android.graphics.Color
import androidx.annotation.StringRes import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.BaseTracker
import eu.kanade.tachiyomi.data.track.DeletableTracker import eu.kanade.tachiyomi.data.track.DeletableTracker
import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.copyTo import eu.kanade.tachiyomi.data.track.mangaupdates.dto.copyTo
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.toTrackSearch import eu.kanade.tachiyomi.data.track.mangaupdates.dto.toTrackSearch
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
class MangaUpdates(id: Long) : Tracker(id, "MangaUpdates"), DeletableTracker { class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker {
companion object { companion object {
const val READING_LIST = 0 const val READING_LIST = 0

View File

@ -4,14 +4,14 @@ import android.graphics.Color
import androidx.annotation.StringRes import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.BaseTracker
import eu.kanade.tachiyomi.data.track.DeletableTracker import eu.kanade.tachiyomi.data.track.DeletableTracker
import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
class MyAnimeList(id: Long) : Tracker(id, "MyAnimeList"), DeletableTracker { class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker {
companion object { companion object {
const val READING = 1 const val READING = 1

View File

@ -4,14 +4,14 @@ import android.graphics.Color
import androidx.annotation.StringRes import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.BaseTracker
import eu.kanade.tachiyomi.data.track.DeletableTracker import eu.kanade.tachiyomi.data.track.DeletableTracker
import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
class Shikimori(id: Long) : Tracker(id, "Shikimori"), DeletableTracker { class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker {
companion object { companion object {
const val READING = 1 const val READING = 1

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.data.track.shikimori package eu.kanade.tachiyomi.data.track.shikimori
import android.net.Uri
import androidx.core.net.toUri import androidx.core.net.toUri
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
@ -37,12 +38,12 @@ class ShikimoriApi(
private val authClient = client.newBuilder().addInterceptor(interceptor).build() private val authClient = client.newBuilder().addInterceptor(interceptor).build()
suspend fun addLibManga(track: Track, user_id: String): Track { suspend fun addLibManga(track: Track, userId: String): Track {
return withIOContext { return withIOContext {
with(json) { with(json) {
val payload = buildJsonObject { val payload = buildJsonObject {
putJsonObject("user_rate") { putJsonObject("user_rate") {
put("user_id", user_id) put("user_id", userId)
put("target_id", track.media_id) put("target_id", track.media_id)
put("target_type", "Manga") put("target_type", "Manga")
put("chapters", track.last_chapter_read.toInt()) put("chapters", track.last_chapter_read.toInt())
@ -65,7 +66,7 @@ class ShikimoriApi(
} }
} }
suspend fun updateLibManga(track: Track, user_id: String): Track = addLibManga(track, user_id) suspend fun updateLibManga(track: Track, userId: String): Track = addLibManga(track, userId)
suspend fun deleteLibManga(track: Track): Track { suspend fun deleteLibManga(track: Track): Track {
return withIOContext { return withIOContext {
@ -194,14 +195,14 @@ class ShikimoriApi(
private const val clientId = "1aaf4cf232372708e98b5abc813d795b539c5a916dbbfe9ac61bf02a360832cc" private const val clientId = "1aaf4cf232372708e98b5abc813d795b539c5a916dbbfe9ac61bf02a360832cc"
private const val clientSecret = "229942c742dd4cde803125d17d64501d91c0b12e14cb1e5120184d77d67024c0" private const val clientSecret = "229942c742dd4cde803125d17d64501d91c0b12e14cb1e5120184d77d67024c0"
private const val baseUrl = "https://shikimori.me" private const val baseUrl = "https://shikimori.one"
private const val apiUrl = "$baseUrl/api" private const val apiUrl = "$baseUrl/api"
private const val oauthUrl = "$baseUrl/oauth/token" private const val oauthUrl = "$baseUrl/oauth/token"
private const val loginUrl = "$baseUrl/oauth/authorize" private const val loginUrl = "$baseUrl/oauth/authorize"
private const val redirectUrl = "tachiyomi://shikimori-auth" private const val redirectUrl = "tachiyomi://shikimori-auth"
fun authUrl() = loginUrl.toUri().buildUpon() fun authUrl(): Uri = loginUrl.toUri().buildUpon()
.appendQueryParameter("client_id", clientId) .appendQueryParameter("client_id", clientId)
.appendQueryParameter("redirect_uri", redirectUrl) .appendQueryParameter("redirect_uri", redirectUrl)
.appendQueryParameter("response_type", "code") .appendQueryParameter("response_type", "code")

View File

@ -4,14 +4,14 @@ import android.graphics.Color
import androidx.annotation.StringRes import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.BaseTracker
import eu.kanade.tachiyomi.data.track.EnhancedTracker import eu.kanade.tachiyomi.data.track.EnhancedTracker
import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import tachiyomi.domain.manga.model.Manga as DomainManga import tachiyomi.domain.manga.model.Manga as DomainManga
import tachiyomi.domain.track.model.Track as DomainTrack import tachiyomi.domain.track.model.Track as DomainTrack
class Suwayomi(id: Long) : Tracker(id, "Suwayomi"), EnhancedTracker { class Suwayomi(id: Long) : BaseTracker(id, "Suwayomi"), EnhancedTracker {
val api by lazy { SuwayomiApi(id) } val api by lazy { SuwayomiApi(id) }

View File

@ -0,0 +1,115 @@
package eu.kanade.tachiyomi.dev.preview
import android.graphics.Color
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import okhttp3.OkHttpClient
import tachiyomi.domain.track.model.Track
data class DummyTracker(
override val id: Long,
override val name: String,
override val supportsReadingDates: Boolean = false,
override val isLoggedIn: Boolean = false,
val valLogoColor: Int = Color.rgb(18, 25, 35),
val valLogo: Int = R.drawable.ic_tracker_anilist,
val valStatuses: List<Int> = (1..6).toList(),
val valReadingStatus: Int = 1,
val valRereadingStatus: Int = 1,
val valCompletionStatus: Int = 2,
val valScoreList: List<String> = (0..10).map(Int::toString),
val val10PointScore: Double = 5.4,
val valSearchResults: List<TrackSearch> = listOf(),
) : Tracker {
override val client: OkHttpClient
get() = TODO("Not yet implemented")
override fun getLogoColor(): Int = valLogoColor
override fun getLogo(): Int = valLogo
override fun getStatusList(): List<Int> = valStatuses
override fun getStatus(status: Int): Int? = when (status) {
1 -> R.string.reading
2 -> R.string.plan_to_read
3 -> R.string.completed
4 -> R.string.on_hold
5 -> R.string.dropped
6 -> R.string.repeating
else -> null
}
override fun getReadingStatus(): Int = valReadingStatus
override fun getRereadingStatus(): Int = valRereadingStatus
override fun getCompletionStatus(): Int = valCompletionStatus
override fun getScoreList(): List<String> = valScoreList
override fun get10PointScore(track: Track): Double = val10PointScore
override fun indexToScore(index: Int): Float = getScoreList()[index].toFloat()
override fun displayScore(track: eu.kanade.tachiyomi.data.database.models.Track): String =
track.score.toString()
override suspend fun update(
track: eu.kanade.tachiyomi.data.database.models.Track,
didReadChapter: Boolean,
): eu.kanade.tachiyomi.data.database.models.Track = track
override suspend fun bind(
track: eu.kanade.tachiyomi.data.database.models.Track,
hasReadChapters: Boolean,
): eu.kanade.tachiyomi.data.database.models.Track = track
override suspend fun search(query: String): List<TrackSearch> = valSearchResults
override suspend fun refresh(
track: eu.kanade.tachiyomi.data.database.models.Track,
): eu.kanade.tachiyomi.data.database.models.Track = track
override suspend fun login(username: String, password: String) = Unit
override fun logout() = Unit
override fun getUsername(): String = "username"
override fun getPassword(): String = "passw0rd"
override fun saveCredentials(username: String, password: String) = Unit
override suspend fun register(
item: eu.kanade.tachiyomi.data.database.models.Track,
mangaId: Long,
) = Unit
override suspend fun setRemoteStatus(
track: eu.kanade.tachiyomi.data.database.models.Track,
status: Int,
) = Unit
override suspend fun setRemoteLastChapterRead(
track: eu.kanade.tachiyomi.data.database.models.Track,
chapterNumber: Int,
) = Unit
override suspend fun setRemoteScore(
track: eu.kanade.tachiyomi.data.database.models.Track,
scoreString: String,
) = Unit
override suspend fun setRemoteStartDate(
track: eu.kanade.tachiyomi.data.database.models.Track,
epochMillis: Long,
) = Unit
override suspend fun setRemoteFinishDate(
track: eu.kanade.tachiyomi.data.database.models.Track,
epochMillis: Long,
) = Unit
}

View File

@ -14,6 +14,7 @@ import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.util.lang.Hash import eu.kanade.tachiyomi.util.lang.Hash
import eu.kanade.tachiyomi.util.storage.copyAndSetReadOnlyTo
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -97,7 +98,8 @@ internal object ExtensionLoader {
val target = File(getPrivateExtensionDir(context), "${extension.packageName}.$PRIVATE_EXTENSION_EXTENSION") val target = File(getPrivateExtensionDir(context), "${extension.packageName}.$PRIVATE_EXTENSION_EXTENSION")
return try { return try {
file.copyTo(target, overwrite = true) target.delete()
file.copyAndSetReadOnlyTo(target, overwrite = true)
if (currentExtension != null) { if (currentExtension != null) {
ExtensionInstallReceiver.notifyReplaced(context, extension.packageName) ExtensionInstallReceiver.notifyReplaced(context, extension.packageName)
} else { } else {

View File

@ -14,7 +14,7 @@ import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
import cafe.adriel.voyager.navigator.tab.TabOptions import cafe.adriel.voyager.navigator.tab.TabOptions
import eu.kanade.presentation.components.TabbedScreen import eu.kanade.presentation.components.TabbedScreen
import eu.kanade.presentation.extensions.RequestStoragePermission import eu.kanade.presentation.permissions.PermissionRequestHelper
import eu.kanade.presentation.util.Tab import eu.kanade.presentation.util.Tab
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel
@ -23,7 +23,6 @@ import eu.kanade.tachiyomi.ui.browse.migration.sources.migrateSourceTab
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen
import eu.kanade.tachiyomi.ui.browse.source.sourcesTab import eu.kanade.tachiyomi.ui.browse.source.sourcesTab
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.storage.DiskUtil
data class BrowseTab( data class BrowseTab(
private val toExtensions: Boolean = false, private val toExtensions: Boolean = false,
@ -66,7 +65,7 @@ data class BrowseTab(
) )
// For local source // For local source
DiskUtil.RequestStoragePermission() PermissionRequestHelper.requestStoragePermission()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
(context as? MainActivity)?.ready = true (context as? MainActivity)?.ready = true

View File

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.browse.extension
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.extension.interactor.GetExtensionLanguages import eu.kanade.domain.extension.interactor.GetExtensionLanguages
import eu.kanade.domain.source.interactor.ToggleLanguage import eu.kanade.domain.source.interactor.ToggleLanguage
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
@ -29,7 +29,7 @@ class ExtensionFilterScreenModel(
val events: Flow<ExtensionFilterEvent> = _events.receiveAsFlow() val events: Flow<ExtensionFilterEvent> = _events.receiveAsFlow()
init { init {
coroutineScope.launch { screenModelScope.launch {
combine( combine(
getExtensionLanguages.subscribe(), getExtensionLanguages.subscribe(),
preferences.enabledLanguages().changes(), preferences.enabledLanguages().changes(),

View File

@ -4,7 +4,7 @@ import android.app.Application
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.extension.interactor.GetExtensionsByType import eu.kanade.domain.extension.interactor.GetExtensionsByType
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.presentation.components.SEARCH_DEBOUNCE_MILLIS import eu.kanade.presentation.components.SEARCH_DEBOUNCE_MILLIS
@ -74,7 +74,7 @@ class ExtensionsScreenModel(
} }
} }
coroutineScope.launchIO { screenModelScope.launchIO {
combine( combine(
state.map { it.searchQuery }.distinctUntilChanged().debounce(SEARCH_DEBOUNCE_MILLIS), state.map { it.searchQuery }.distinctUntilChanged().debounce(SEARCH_DEBOUNCE_MILLIS),
_currentDownloads, _currentDownloads,
@ -118,11 +118,11 @@ class ExtensionsScreenModel(
} }
} }
coroutineScope.launchIO { findAvailableExtensions() } screenModelScope.launchIO { findAvailableExtensions() }
preferences.extensionUpdatesCount().changes() preferences.extensionUpdatesCount().changes()
.onEach { mutableState.update { state -> state.copy(updates = it) } } .onEach { mutableState.update { state -> state.copy(updates = it) } }
.launchIn(coroutineScope) .launchIn(screenModelScope)
} }
fun search(query: String?) { fun search(query: String?) {
@ -132,7 +132,7 @@ class ExtensionsScreenModel(
} }
fun updateAllExtensions() { fun updateAllExtensions() {
coroutineScope.launchIO { screenModelScope.launchIO {
state.value.items.values.flatten() state.value.items.values.flatten()
.map { it.extension } .map { it.extension }
.filterIsInstance<Extension.Installed>() .filterIsInstance<Extension.Installed>()
@ -142,13 +142,13 @@ class ExtensionsScreenModel(
} }
fun installExtension(extension: Extension.Available) { fun installExtension(extension: Extension.Available) {
coroutineScope.launchIO { screenModelScope.launchIO {
extensionManager.installExtension(extension).collectToInstallUpdate(extension) extensionManager.installExtension(extension).collectToInstallUpdate(extension)
} }
} }
fun updateExtension(extension: Extension.Installed) { fun updateExtension(extension: Extension.Installed) {
coroutineScope.launchIO { screenModelScope.launchIO {
extensionManager.updateExtension(extension).collectToInstallUpdate(extension) extensionManager.updateExtension(extension).collectToInstallUpdate(extension)
} }
} }
@ -176,7 +176,7 @@ class ExtensionsScreenModel(
} }
fun findAvailableExtensions() { fun findAvailableExtensions() {
coroutineScope.launchIO { screenModelScope.launchIO {
mutableState.update { it.copy(isRefreshing = true) } mutableState.update { it.copy(isRefreshing = true) }
extensionManager.findAvailableExtensions() extensionManager.findAvailableExtensions()

View File

@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.ui.browse.extension.details
import android.content.Context import android.content.Context
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.extension.interactor.ExtensionSourceItem import eu.kanade.domain.extension.interactor.ExtensionSourceItem
import eu.kanade.domain.extension.interactor.GetExtensionSources import eu.kanade.domain.extension.interactor.GetExtensionSources
import eu.kanade.domain.source.interactor.ToggleSource import eu.kanade.domain.source.interactor.ToggleSource
@ -44,7 +44,7 @@ class ExtensionDetailsScreenModel(
val events: Flow<ExtensionDetailsEvent> = _events.receiveAsFlow() val events: Flow<ExtensionDetailsEvent> = _events.receiveAsFlow()
init { init {
coroutineScope.launch { screenModelScope.launch {
launch { launch {
extensionManager.installedExtensionsFlow extensionManager.installedExtensionsFlow
.map { it.firstOrNull { extension -> extension.pkgName == pkgName } } .map { it.firstOrNull { extension -> extension.pkgName == pkgName } }

View File

@ -7,9 +7,6 @@ import android.view.View
import androidx.appcompat.view.ContextThemeWrapper import androidx.appcompat.view.ContextThemeWrapper
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
@ -34,7 +31,7 @@ import androidx.preference.forEach
import androidx.preference.getOnBindEditTextListener import androidx.preference.getOnBindEditTextListener
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.components.UpIcon import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.util.Screen import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore
@ -55,13 +52,9 @@ class SourcePreferencesScreen(val sourceId: Long) : Screen() {
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( AppBar(
title = { Text(text = Injekt.get<SourceManager>().getOrStub(sourceId).toString()) }, title = Injekt.get<SourceManager>().getOrStub(sourceId).toString(),
navigationIcon = { navigateUp = navigator::pop,
IconButton(onClick = navigator::pop) {
UpIcon()
}
},
scrollBehavior = it, scrollBehavior = it,
) )
}, },

View File

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.browse.migration.manga
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -30,7 +30,7 @@ class MigrateMangaScreenModel(
val events: Flow<MigrationMangaEvent> = _events.receiveAsFlow() val events: Flow<MigrationMangaEvent> = _events.receiveAsFlow()
init { init {
coroutineScope.launch { screenModelScope.launch {
mutableState.update { state -> mutableState.update { state ->
state.copy(source = sourceManager.getOrStub(sourceId)) state.copy(source = sourceManager.getOrStub(sourceId))
} }

View File

@ -1,17 +1,13 @@
package eu.kanade.tachiyomi.ui.browse.migration.search package eu.kanade.tachiyomi.ui.browse.migration.search
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
@ -22,9 +18,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.toMutableStateList import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
@ -55,6 +49,7 @@ import tachiyomi.domain.manga.model.MangaUpdate
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
import tachiyomi.domain.track.interactor.GetTracks import tachiyomi.domain.track.interactor.GetTracks
import tachiyomi.domain.track.interactor.InsertTrack import tachiyomi.domain.track.interactor.InsertTrack
import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.screens.LoadingScreen import tachiyomi.presentation.core.screens.LoadingScreen
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -69,7 +64,6 @@ internal fun MigrateDialog(
onClickTitle: () -> Unit, onClickTitle: () -> Unit,
onPopScreen: () -> Unit, onPopScreen: () -> Unit,
) { ) {
val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val state by screenModel.state.collectAsState() val state by screenModel.state.collectAsState()
@ -92,16 +86,11 @@ internal fun MigrateDialog(
modifier = Modifier.verticalScroll(rememberScrollState()), modifier = Modifier.verticalScroll(rememberScrollState()),
) { ) {
flags.forEachIndexed { index, flag -> flags.forEachIndexed { index, flag ->
val onChange = { selectedFlags[index] = !selectedFlags[index] } LabeledCheckbox(
Row( label = stringResource(flag.titleId),
modifier = Modifier checked = selectedFlags[index],
.fillMaxWidth() onCheckedChange = { selectedFlags[index] = it },
.clickable(onClick = onChange), )
verticalAlignment = Alignment.CenterVertically,
) {
Checkbox(checked = selectedFlags[index], onCheckedChange = { onChange() })
Text(text = context.getString(flag.titleId))
}
} }
} }
}, },

View File

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.browse.migration.search
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope import cafe.adriel.voyager.core.model.screenModelScope
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import tachiyomi.domain.manga.interactor.GetManga import tachiyomi.domain.manga.interactor.GetManga
@ -16,7 +16,7 @@ class MigrateSearchScreenDialogScreenModel(
) : StateScreenModel<MigrateSearchScreenDialogScreenModel.State>(State()) { ) : StateScreenModel<MigrateSearchScreenDialogScreenModel.State>(State()) {
init { init {
coroutineScope.launch { screenModelScope.launch {
val manga = getManga.await(mangaId)!! val manga = getManga.await(mangaId)!!
mutableState.update { mutableState.update {

View File

@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.ui.browse.migration.search package eu.kanade.tachiyomi.ui.browse.migration.search
import cafe.adriel.voyager.core.model.coroutineScope import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
@ -16,7 +16,7 @@ class MigrateSearchScreenModel(
) : SearchScreenModel() { ) : SearchScreenModel() {
init { init {
coroutineScope.launch { screenModelScope.launch {
val manga = getManga.await(mangaId)!! val manga = getManga.await(mangaId)!!
mutableState.update { mutableState.update {
it.copy( it.copy(

View File

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.browse.migration.sources
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount
import eu.kanade.domain.source.interactor.SetMigrateSorting import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
@ -30,7 +30,7 @@ class MigrateSourceScreenModel(
val channel = _channel.receiveAsFlow() val channel = _channel.receiveAsFlow()
init { init {
coroutineScope.launchIO { screenModelScope.launchIO {
getSourcesWithFavoriteCount.subscribe() getSourcesWithFavoriteCount.subscribe()
.catch { .catch {
logcat(LogPriority.ERROR, it) logcat(LogPriority.ERROR, it)
@ -48,11 +48,11 @@ class MigrateSourceScreenModel(
preferences.migrationSortingDirection().changes() preferences.migrationSortingDirection().changes()
.onEach { mutableState.update { state -> state.copy(sortingDirection = it) } } .onEach { mutableState.update { state -> state.copy(sortingDirection = it) } }
.launchIn(coroutineScope) .launchIn(screenModelScope)
preferences.migrationSortingMode().changes() preferences.migrationSortingMode().changes()
.onEach { mutableState.update { state -> state.copy(sortingMode = it) } } .onEach { mutableState.update { state -> state.copy(sortingMode = it) } }
.launchIn(coroutineScope) .launchIn(screenModelScope)
} }
fun toggleSortingMode() { fun toggleSortingMode() {

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.browse.migration.sources package eu.kanade.tachiyomi.ui.browse.migration.sources
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.outlined.HelpOutline import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@ -29,7 +30,7 @@ fun Screen.migrateSourceTab(): TabContent {
actions = listOf( actions = listOf(
AppBar.Action( AppBar.Action(
title = stringResource(R.string.migration_help_guide), title = stringResource(R.string.migration_help_guide),
icon = Icons.Outlined.HelpOutline, icon = Icons.AutoMirrored.Outlined.HelpOutline,
onClick = { onClick = {
uriHandler.openUri("https://tachiyomi.org/docs/guides/source-migration") uriHandler.openUri("https://tachiyomi.org/docs/guides/source-migration")
}, },

View File

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.browse.source
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.source.interactor.GetLanguagesWithSources import eu.kanade.domain.source.interactor.GetLanguagesWithSources
import eu.kanade.domain.source.interactor.ToggleLanguage import eu.kanade.domain.source.interactor.ToggleLanguage
import eu.kanade.domain.source.interactor.ToggleSource import eu.kanade.domain.source.interactor.ToggleSource
@ -25,7 +25,7 @@ class SourcesFilterScreenModel(
) : StateScreenModel<SourcesFilterScreenModel.State>(State.Loading) { ) : StateScreenModel<SourcesFilterScreenModel.State>(State.Loading) {
init { init {
coroutineScope.launch { screenModelScope.launch {
combine( combine(
getLanguagesWithSources.subscribe(), getLanguagesWithSources.subscribe(),
preferences.enabledLanguages().changes(), preferences.enabledLanguages().changes(),

View File

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.browse.source
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.source.interactor.GetEnabledSources import eu.kanade.domain.source.interactor.GetEnabledSources
import eu.kanade.domain.source.interactor.ToggleSource import eu.kanade.domain.source.interactor.ToggleSource
import eu.kanade.domain.source.interactor.ToggleSourcePin import eu.kanade.domain.source.interactor.ToggleSourcePin
@ -31,7 +31,7 @@ class SourcesScreenModel(
val events = _events.receiveAsFlow() val events = _events.receiveAsFlow()
init { init {
coroutineScope.launchIO { screenModelScope.launchIO {
getEnabledSources.subscribe() getEnabledSources.subscribe()
.catch { .catch {
logcat(LogPriority.ERROR, it) logcat(LogPriority.ERROR, it)

View File

@ -12,7 +12,7 @@ import androidx.paging.cachedIn
import androidx.paging.filter import androidx.paging.filter
import androidx.paging.map import androidx.paging.map
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.core.preference.asState import eu.kanade.core.preference.asState
import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.interactor.UpdateManga
@ -72,7 +72,7 @@ class BrowseSourceScreenModel(
private val addTracks: AddTracks = Injekt.get(), private val addTracks: AddTracks = Injekt.get(),
) : StateScreenModel<BrowseSourceScreenModel.State>(State(Listing.valueOf(listingQuery))) { ) : StateScreenModel<BrowseSourceScreenModel.State>(State(Listing.valueOf(listingQuery))) {
var displayMode by sourcePreferences.sourceDisplayMode().asState(coroutineScope) var displayMode by sourcePreferences.sourceDisplayMode().asState(screenModelScope)
val source = sourceManager.getOrStub(sourceId) val source = sourceManager.getOrStub(sourceId)
@ -220,7 +220,7 @@ class BrowseSourceScreenModel(
* @param manga the manga to update. * @param manga the manga to update.
*/ */
fun changeMangaFavorite(manga: Manga) { fun changeMangaFavorite(manga: Manga) {
coroutineScope.launch { screenModelScope.launch {
var new = manga.copy( var new = manga.copy(
favorite = !manga.favorite, favorite = !manga.favorite,
dateAdded = when (manga.favorite) { dateAdded = when (manga.favorite) {
@ -241,7 +241,7 @@ class BrowseSourceScreenModel(
} }
fun addFavorite(manga: Manga) { fun addFavorite(manga: Manga) {
coroutineScope.launch { screenModelScope.launch {
val categories = getCategories() val categories = getCategories()
val defaultCategoryId = libraryPreferences.defaultCategory().get() val defaultCategoryId = libraryPreferences.defaultCategory().get()
val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() } val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() }
@ -291,7 +291,7 @@ class BrowseSourceScreenModel(
} }
fun moveMangaToCategories(manga: Manga, categoryIds: List<Long>) { fun moveMangaToCategories(manga: Manga, categoryIds: List<Long>) {
coroutineScope.launchIO { screenModelScope.launchIO {
setMangaCategories.await( setMangaCategories.await(
mangaId = manga.id, mangaId = manga.id,
categoryIds = categoryIds.toList(), categoryIds = categoryIds.toList(),

View File

@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.ui.category
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
@ -31,7 +31,7 @@ class CategoryScreenModel(
val events = _events.receiveAsFlow() val events = _events.receiveAsFlow()
init { init {
coroutineScope.launch { screenModelScope.launch {
getCategories.subscribe() getCategories.subscribe()
.collectLatest { categories -> .collectLatest { categories ->
mutableState.update { mutableState.update {
@ -44,7 +44,7 @@ class CategoryScreenModel(
} }
fun createCategory(name: String) { fun createCategory(name: String) {
coroutineScope.launch { screenModelScope.launch {
when (createCategoryWithName.await(name)) { when (createCategoryWithName.await(name)) {
is CreateCategoryWithName.Result.InternalError -> _events.send(CategoryEvent.InternalError) is CreateCategoryWithName.Result.InternalError -> _events.send(CategoryEvent.InternalError)
else -> {} else -> {}
@ -53,7 +53,7 @@ class CategoryScreenModel(
} }
fun deleteCategory(categoryId: Long) { fun deleteCategory(categoryId: Long) {
coroutineScope.launch { screenModelScope.launch {
when (deleteCategory.await(categoryId = categoryId)) { when (deleteCategory.await(categoryId = categoryId)) {
is DeleteCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError) is DeleteCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError)
else -> {} else -> {}
@ -62,7 +62,7 @@ class CategoryScreenModel(
} }
fun sortAlphabetically() { fun sortAlphabetically() {
coroutineScope.launch { screenModelScope.launch {
when (reorderCategory.sortAlphabetically()) { when (reorderCategory.sortAlphabetically()) {
is ReorderCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError) is ReorderCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError)
else -> {} else -> {}
@ -71,7 +71,7 @@ class CategoryScreenModel(
} }
fun moveUp(category: Category) { fun moveUp(category: Category) {
coroutineScope.launch { screenModelScope.launch {
when (reorderCategory.moveUp(category)) { when (reorderCategory.moveUp(category)) {
is ReorderCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError) is ReorderCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError)
else -> {} else -> {}
@ -80,7 +80,7 @@ class CategoryScreenModel(
} }
fun moveDown(category: Category) { fun moveDown(category: Category) {
coroutineScope.launch { screenModelScope.launch {
when (reorderCategory.moveDown(category)) { when (reorderCategory.moveDown(category)) {
is ReorderCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError) is ReorderCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError)
else -> {} else -> {}
@ -89,7 +89,7 @@ class CategoryScreenModel(
} }
fun renameCategory(category: Category, name: String) { fun renameCategory(category: Category, name: String) {
coroutineScope.launch { screenModelScope.launch {
when (renameCategory.await(category, name)) { when (renameCategory.await(category, name)) {
is RenameCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError) is RenameCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError)
else -> {} else -> {}

View File

@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
@ -14,6 +15,7 @@ import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.screens.LoadingScreen import tachiyomi.presentation.core.screens.LoadingScreen
@ -23,6 +25,7 @@ class DeepLinkScreen(
@Composable @Composable
override fun Content() { override fun Content() {
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { val screenModel = rememberScreenModel {
@ -46,12 +49,22 @@ class DeepLinkScreen(
navigator.replace(GlobalSearchScreen(query)) navigator.replace(GlobalSearchScreen(query))
} }
is DeepLinkScreenModel.State.Result -> { is DeepLinkScreenModel.State.Result -> {
navigator.replace( val resultState = state as DeepLinkScreenModel.State.Result
MangaScreen( if (resultState.chapterId == null) {
(state as DeepLinkScreenModel.State.Result).manga.id, navigator.replace(
true, MangaScreen(
), resultState.manga.id,
) true,
),
)
} else {
navigator.pop()
ReaderActivity.newIntent(
context,
resultState.manga.id,
resultState.chapterId,
).also(context::startActivity)
}
} }
} }
} }

View File

@ -2,11 +2,21 @@ package eu.kanade.tachiyomi.ui.deeplink
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
import eu.kanade.domain.manga.model.toDomainManga import eu.kanade.domain.manga.model.toDomainManga
import eu.kanade.domain.manga.model.toSManga
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ResolvableSource import eu.kanade.tachiyomi.source.online.ResolvableSource
import eu.kanade.tachiyomi.source.online.UriType
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import tachiyomi.core.util.lang.launchIO import tachiyomi.core.util.lang.launchIO
import tachiyomi.domain.chapter.interactor.GetChapterByUrlAndMangaId
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.manga.interactor.GetMangaByUrlAndSourceId
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@ -15,25 +25,59 @@ import uy.kohesive.injekt.api.get
class DeepLinkScreenModel( class DeepLinkScreenModel(
query: String = "", query: String = "",
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
private val getChapterByUrlAndMangaId: GetChapterByUrlAndMangaId = Injekt.get(),
private val getMangaByUrlAndSourceId: GetMangaByUrlAndSourceId = Injekt.get(),
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(),
) : StateScreenModel<DeepLinkScreenModel.State>(State.Loading) { ) : StateScreenModel<DeepLinkScreenModel.State>(State.Loading) {
init { init {
coroutineScope.launchIO { screenModelScope.launchIO {
val manga = sourceManager.getCatalogueSources() val source = sourceManager.getCatalogueSources()
.filterIsInstance<ResolvableSource>() .filterIsInstance<ResolvableSource>()
.filter { it.canResolveUri(query) } .firstOrNull { it.getUriType(query) != UriType.Unknown }
.firstNotNullOfOrNull { it.getManga(query)?.toDomainManga(it.id) }
val manga = source?.getManga(query)?.let {
getMangaFromSManga(it, source.id)
}
val chapter = if (source?.getUriType(query) == UriType.Chapter && manga != null) {
source.getChapter(query)?.let { getChapterFromSChapter(it, manga, source) }
} else {
null
}
mutableState.update { mutableState.update {
if (manga == null) { if (manga == null) {
State.NoResults State.NoResults
} else { } else {
State.Result(manga) if (chapter == null) {
State.Result(manga)
} else {
State.Result(manga, chapter.id)
}
} }
} }
} }
} }
private suspend fun getChapterFromSChapter(sChapter: SChapter, manga: Manga, source: Source): Chapter? {
val localChapter = getChapterByUrlAndMangaId.await(sChapter.url, manga.id)
return if (localChapter == null) {
val sourceChapters = source.getChapterList(manga.toSManga())
val newChapters = syncChaptersWithSource.await(sourceChapters, manga, source, false)
newChapters.find { it.url == sChapter.url }
} else {
localChapter
}
}
private suspend fun getMangaFromSManga(sManga: SManga, sourceId: Long): Manga {
return getMangaByUrlAndSourceId.awaitManga(sManga.url, sourceId)
?: networkToLocalManga.await(sManga.toDomainManga(sourceId))
}
sealed interface State { sealed interface State {
@Immutable @Immutable
data object Loading : State data object Loading : State
@ -42,6 +86,6 @@ class DeepLinkScreenModel(
data object NoResults : State data object NoResults : State
@Immutable @Immutable
data class Result(val manga: Manga) : State data class Result(val manga: Manga, val chapterId: Long? = null) : State
} }
} }

View File

@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.Sort
import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.outlined.Pause import androidx.compose.material.icons.outlined.Pause
import androidx.compose.material.icons.outlined.Sort import androidx.compose.material.icons.outlined.Sort
@ -185,7 +186,7 @@ object DownloadQueueScreen : Screen() {
listOf( listOf(
AppBar.Action( AppBar.Action(
title = stringResource(R.string.action_sort), title = stringResource(R.string.action_sort),
icon = Icons.Outlined.Sort, icon = Icons.AutoMirrored.Outlined.Sort,
onClick = { sortExpanded = true }, onClick = { sortExpanded = true },
), ),
AppBar.OverflowAction( AppBar.OverflowAction(

View File

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.download
import android.view.MenuItem import android.view.MenuItem
import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.coroutineScope import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
@ -114,7 +114,7 @@ class DownloadQueueScreenModel(
} }
init { init {
coroutineScope.launch { screenModelScope.launch {
downloadManager.queueState downloadManager.queueState
.map { downloads -> .map { downloads ->
downloads downloads
@ -208,7 +208,7 @@ class DownloadQueueScreenModel(
* @param download the download to observe its progress. * @param download the download to observe its progress.
*/ */
private fun launchProgressJob(download: Download) { private fun launchProgressJob(download: Download) {
val job = coroutineScope.launch { val job = screenModelScope.launch {
while (download.pages == null) { while (download.pages == null) {
delay(50) delay(50)
} }

View File

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.history
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.core.util.insertSeparators import eu.kanade.core.util.insertSeparators
import eu.kanade.presentation.history.HistoryUiModel import eu.kanade.presentation.history.HistoryUiModel
import eu.kanade.tachiyomi.util.lang.toDateKey import eu.kanade.tachiyomi.util.lang.toDateKey
@ -40,7 +40,7 @@ class HistoryScreenModel(
val events: Flow<Event> = _events.receiveAsFlow() val events: Flow<Event> = _events.receiveAsFlow()
init { init {
coroutineScope.launch { screenModelScope.launch {
state.map { it.searchQuery } state.map { it.searchQuery }
.distinctUntilChanged() .distinctUntilChanged()
.flatMapLatest { query -> .flatMapLatest { query ->
@ -75,7 +75,7 @@ class HistoryScreenModel(
} }
fun getNextChapterForManga(mangaId: Long, chapterId: Long) { fun getNextChapterForManga(mangaId: Long, chapterId: Long) {
coroutineScope.launchIO { screenModelScope.launchIO {
sendNextChapterEvent(getNextChapters.await(mangaId, chapterId, onlyUnread = false)) sendNextChapterEvent(getNextChapters.await(mangaId, chapterId, onlyUnread = false))
} }
} }
@ -86,19 +86,19 @@ class HistoryScreenModel(
} }
fun removeFromHistory(history: HistoryWithRelations) { fun removeFromHistory(history: HistoryWithRelations) {
coroutineScope.launchIO { screenModelScope.launchIO {
removeHistory.await(history) removeHistory.await(history)
} }
} }
fun removeAllFromHistory(mangaId: Long) { fun removeAllFromHistory(mangaId: Long) {
coroutineScope.launchIO { screenModelScope.launchIO {
removeHistory.await(mangaId) removeHistory.await(mangaId)
} }
} }
fun removeAllHistory() { fun removeAllHistory() {
coroutineScope.launchIO { screenModelScope.launchIO {
val result = removeHistory.awaitAll() val result = removeHistory.awaitAll()
if (!result) return@launchIO if (!result) return@launchIO
_events.send(Event.HistoryCleared) _events.send(Event.HistoryCleared)

Some files were not shown because too many files have changed in this diff Show More