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"],
"packageRules": [
{
"managers": ["maven"],
"packageNames": ["com.google.guava:guava"],
"versionScheme": "docker"
},
{
// Compiler plugins are tightly coupled to Kotlin version
"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)
- Emulator or phone with developer options enabled to test changes.
## Linting
To auto-fix some linting errors, run the `ktlintFormat` Gradle task.
## Getting help
- 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)
* 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 screenshot (if needed)
* If it could be device-dependent, try reproducing on another device (if possible)

1
app/.gitignore vendored
View File

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

View File

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

View File

@ -1,5 +1,4 @@
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut
android:enabled="true"
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.chapter.interactor.GetChapter
import tachiyomi.domain.chapter.interactor.GetChapterByMangaId
import tachiyomi.domain.chapter.interactor.GetChapterByUrlAndMangaId
import tachiyomi.domain.chapter.interactor.SetMangaDefaultChapterFlags
import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter
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.GetLibraryManga
import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.interactor.GetMangaByUrlAndSourceId
import tachiyomi.domain.manga.interactor.GetMangaWithChapters
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
import tachiyomi.domain.manga.interactor.ResetViewerFlags
@ -99,6 +101,7 @@ class DomainModule : InjektModule {
addFactory { GetFavorites(get()) }
addFactory { GetLibraryManga(get()) }
addFactory { GetMangaWithChapters(get(), get()) }
addFactory { GetMangaByUrlAndSourceId(get()) }
addFactory { GetManga(get()) }
addFactory { GetNextChapters(get(), get(), get()) }
addFactory { ResetViewerFlags(get()) }
@ -126,6 +129,7 @@ class DomainModule : InjektModule {
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
addFactory { GetChapter(get()) }
addFactory { GetChapterByMangaId(get()) }
addFactory { GetChapterByUrlAndMangaId(get()) }
addFactory { UpdateChapter(get()) }
addFactory { SetReadStatus(get(), get(), get(), get()) }
addFactory { ShouldUpdateDbChapter() }

View File

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

View File

@ -8,21 +8,19 @@ import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
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.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.util.system.workManager
import logcat.LogPriority
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.track.interactor.GetTracks
import tachiyomi.domain.track.interactor.InsertTrack
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.time.Duration.Companion.minutes
import kotlin.time.toJavaDuration
class DelayedTrackingUpdateJob(context: Context, workerParams: WorkerParameters) :
class DelayedTrackingUpdateJob(private val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result {
@ -31,9 +29,8 @@ class DelayedTrackingUpdateJob(context: Context, workerParams: WorkerParameters)
}
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>()
withIOContext {
@ -46,17 +43,8 @@ class DelayedTrackingUpdateJob(context: Context, workerParams: WorkerParameters)
track?.copy(lastChapterRead = it.lastChapterRead.toDouble())
}
.forEach { track ->
try {
val service = trackerManager.get(track.syncId)
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)
}
logcat(LogPriority.DEBUG) { "Updating delayed track item: ${track.mangaId}, last chapter read: ${track.lastChapterRead}" }
trackChapter.await(context, track.mangaId, track.lastChapterRead)
}
}

View File

@ -4,7 +4,6 @@ import android.content.Context
import androidx.core.content.edit
import logcat.LogPriority
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.track.model.Track
class DelayedTrackingStore(context: Context) {
@ -13,13 +12,12 @@ class DelayedTrackingStore(context: Context) {
*/
private val preferences = context.getSharedPreferences("tracking_queue", Context.MODE_PRIVATE)
fun addItem(track: Track) {
val trackId = track.id.toString()
val lastChapterRead = preferences.getFloat(trackId, 0f)
if (track.lastChapterRead > lastChapterRead) {
logcat(LogPriority.DEBUG) { "Queuing track item: $trackId, last chapter read: ${track.lastChapterRead}" }
fun add(trackId: Long, lastChapterRead: Double) {
val previousLastChapterRead = preferences.getFloat(trackId.toString(), 0f)
if (lastChapterRead > previousLastChapterRead) {
logcat(LogPriority.DEBUG) { "Queuing track item: $trackId, last chapter read: $lastChapterRead" }
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.lazy.grid.GridCells
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.Refresh
import androidx.compose.material3.SnackbarDuration
@ -79,7 +79,7 @@ fun BrowseSourceContent(
listOf(
EmptyScreenAction(
stringResId = R.string.local_source_help_guide,
icon = Icons.Outlined.HelpOutline,
icon = Icons.AutoMirrored.Outlined.HelpOutline,
onClick = onLocalSourceHelpClick,
),
)
@ -97,7 +97,7 @@ fun BrowseSourceContent(
),
EmptyScreenAction(
stringResId = R.string.label_help,
icon = Icons.Outlined.HelpOutline,
icon = Icons.AutoMirrored.Outlined.HelpOutline,
onClick = onHelpClick,
),
)

View File

@ -16,7 +16,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.items
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.Settings
import androidx.compose.material3.AlertDialog
@ -92,7 +92,7 @@ fun ExtensionDetailsScreen(
add(
AppBar.Action(
title = stringResource(R.string.action_faq_and_guides),
icon = Icons.Outlined.HelpOutline,
icon = Icons.AutoMirrored.Outlined.HelpOutline,
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(
extension = extension,
modifier = Modifier

View File

@ -1,7 +1,7 @@
package eu.kanade.presentation.browse.components
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.material3.Text
import androidx.compose.material3.TopAppBarScrollBehavior
@ -56,7 +56,7 @@ fun BrowseSourceToolbar(
actions = listOfNotNull(
AppBar.Action(
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 },
),
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.size
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.Error
import androidx.compose.material3.CircularProgressIndicator
@ -54,7 +55,7 @@ fun GlobalSearchResultItem(
Text(text = subtitle)
}
IconButton(onClick = onClick) {
Icon(imageVector = Icons.Outlined.ArrowForward, contentDescription = null)
Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null)
}
}
content()

View File

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

View File

@ -8,17 +8,11 @@ import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowBack
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.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
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.CategoryListItem
import eu.kanade.presentation.components.AppBar
@ -46,21 +40,9 @@ fun CategoryScreen(
val lazyListState = rememberLazyListState()
Scaffold(
topBar = { scrollBehavior ->
TopAppBar(
title = {
Text(
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),
)
}
},
AppBar(
title = stringResource(R.string.action_edit_categories),
navigateUp = navigateUp,
actions = {
AppBarActions(
listOf(

View File

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

View File

@ -1,5 +1,6 @@
package eu.kanade.presentation.components
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
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.material.TextFieldDefaults
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material.icons.outlined.ArrowForward
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.MoreVert
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.vector.ImageVector
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import eu.kanade.tachiyomi.R
@ -60,6 +58,7 @@ const val SEARCH_DEBOUNCE_MILLIS = 250L
@Composable
fun AppBar(
modifier: Modifier = Modifier,
backgroundColor: Color? = null,
// Text
title: String?,
subtitle: String? = null,
@ -81,6 +80,7 @@ fun AppBar(
AppBar(
modifier = modifier,
backgroundColor = backgroundColor,
titleContent = {
if (isActionMode) {
AppBarTitle(actionModeCounter.toString())
@ -106,6 +106,7 @@ fun AppBar(
@Composable
fun AppBar(
modifier: Modifier = Modifier,
backgroundColor: Color? = null,
// Title
titleContent: @Composable () -> Unit,
// Up button
@ -142,7 +143,7 @@ fun AppBar(
title = titleContent,
actions = actions,
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
containerColor = backgroundColor ?: MaterialTheme.colorScheme.surfaceColorAtElevation(
elevation = if (isActionMode) 3.dp else 0.dp,
),
),
@ -170,6 +171,9 @@ fun AppBarTitle(
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.basicMarquee(
delayMillis = 2_000,
),
)
}
}
@ -363,7 +367,7 @@ fun SearchToolbar(
@Composable
fun UpIcon(navigationIcon: ImageVector? = null) {
val icon = navigationIcon
?: if (LocalLayoutDirection.current == LayoutDirection.Ltr) Icons.Outlined.ArrowBack else Icons.Outlined.ArrowForward
?: Icons.AutoMirrored.Outlined.ArrowBack
Icon(
imageVector = icon,
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.sizeIn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowLeft
import androidx.compose.material.icons.outlined.ArrowRight
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
import androidx.compose.material.icons.outlined.RadioButtonChecked
import androidx.compose.material.icons.outlined.RadioButtonUnchecked
import androidx.compose.material3.DropdownMenuItem
@ -16,10 +15,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.PopupProperties
import eu.kanade.tachiyomi.R
@ -77,14 +74,13 @@ fun NestedMenuItem(
) {
var nestedExpanded by remember { mutableStateOf(false) }
val closeMenu = { nestedExpanded = false }
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
DropdownMenuItem(
text = text,
onClick = { nestedExpanded = true },
trailingIcon = {
Icon(
imageVector = if (isLtr) Icons.Outlined.ArrowRight else Icons.Outlined.ArrowLeft,
imageVector = Icons.AutoMirrored.Outlined.ArrowRight,
contentDescription = null,
)
},

View File

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

View File

@ -14,8 +14,8 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -55,7 +55,7 @@ fun TabbedDialog(
Column {
Row {
TabRow(
PrimaryTabRow(
modifier = Modifier.weight(1f),
selectedTabIndex = pagerState.currentPage,
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.pager.rememberPagerState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
@ -67,7 +67,7 @@ fun TabbedScreen(
end = contentPadding.calculateEndPadding(LocalLayoutDirection.current),
),
) {
TabRow(
PrimaryTabRow(
selectedTabIndex = state.currentPage,
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.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.components.AppBar
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.SearchToolbar
import eu.kanade.presentation.history.components.HistoryItem
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.history.HistoryScreenModel
import tachiyomi.core.preference.InMemoryPreferenceStore
import tachiyomi.domain.history.model.HistoryWithRelations
import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen
import tachiyomi.presentation.core.util.ThemePreviews
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Date
@ -37,6 +41,7 @@ fun HistoryScreen(
onClickCover: (mangaId: Long) -> Unit,
onClickResume: (mangaId: Long, chapterId: Long) -> Unit,
onDialogChange: (HistoryScreenModel.Dialog?) -> Unit,
preferences: UiPreferences = Injekt.get(),
) {
Scaffold(
topBar = { scrollBehavior ->
@ -82,6 +87,7 @@ fun HistoryScreen(
onClickCover = { history -> onClickCover(history.mangaId) },
onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) },
onClickDelete = { item -> onDialogChange(HistoryScreenModel.Dialog.Delete(item)) },
preferences = preferences,
)
}
}
@ -95,7 +101,7 @@ private fun HistoryScreenContent(
onClickCover: (HistoryWithRelations) -> Unit,
onClickResume: (HistoryWithRelations) -> Unit,
onClickDelete: (HistoryWithRelations) -> Unit,
preferences: UiPreferences = Injekt.get(),
preferences: UiPreferences,
) {
val relativeTime = remember { preferences.relativeTime().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 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
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
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.Checkbox
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@ -14,11 +10,12 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.unit.dp
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.util.ThemePreviews
@Composable
fun HistoryDeleteDialog(
@ -32,28 +29,16 @@ fun HistoryDeleteDialog(
Text(text = stringResource(R.string.action_remove))
},
text = {
Column {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(text = stringResource(R.string.dialog_with_checkbox_remove_description))
Row(
modifier = Modifier
.padding(top = 16.dp)
.toggleable(
interactionSource = remember { MutableInteractionSource() },
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),
)
}
LabeledCheckbox(
label = stringResource(R.string.dialog_with_checkbox_reset),
checked = removeEverything,
onCheckedChange = { removeEverything = it },
)
}
},
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.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.manga.components.MangaCover
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.presentation.util.formatChapterNumber
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.lang.toTimestampString
import tachiyomi.domain.history.model.HistoryWithRelations
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
fun HistoryItem(
@ -40,7 +43,7 @@ fun HistoryItem(
Row(
modifier = modifier
.clickable(onClick = onClickResume)
.height(HISTORY_ITEM_HEIGHT)
.height(HistoryItemHeight)
.padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small),
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
import androidx.compose.foundation.clickable
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.Checkbox
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@ -13,11 +9,10 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import eu.kanade.tachiyomi.R
import tachiyomi.core.preference.CheckboxState
import tachiyomi.presentation.core.components.LabeledCheckbox
@Composable
fun DeleteLibraryMangaDialog(
@ -62,27 +57,18 @@ fun DeleteLibraryMangaDialog(
text = {
Column {
list.forEach { state ->
val onCheck = {
val index = list.indexOf(state)
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))
}
LabeledCheckbox(
label = stringResource(state.value),
checked = state.isChecked,
onCheckedChange = {
val index = list.indexOf(state)
if (index != -1) {
val mutableList = list.toMutableList()
mutableList[index] = state.next() as CheckboxState.State<Int>
list = mutableList.toList()
}
},
)
}
}
},

View File

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

View File

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

View File

@ -140,6 +140,7 @@ private fun DownloadingIndicator(
val animatedProgress by animateFloatAsState(
targetValue = downloadProgress / 100f,
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
label = "progress",
)
arrowColor = if (animatedProgress < 0.5f) {
strokeColor
@ -147,7 +148,7 @@ private fun DownloadingIndicator(
MaterialTheme.colorScheme.background
}
CircularProgressIndicator(
progress = animatedProgress,
progress = { animatedProgress },
modifier = IndicatorModifier,
color = strokeColor,
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.shape.ZeroCornerSize
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.BookmarkRemove
import androidx.compose.material.icons.outlined.Delete
@ -258,7 +259,7 @@ fun LibraryBottomActionMenu(
) {
Button(
title = stringResource(R.string.action_move_category),
icon = Icons.Outlined.Label,
icon = Icons.AutoMirrored.Outlined.Label,
toConfirm = confirm[0],
onLongClick = { onLongClickItem(0) },
onClick = onChangeCategoryClicked,

View File

@ -48,7 +48,6 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.vector.ImageVector
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.LocalDensity
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
@ -589,67 +587,70 @@ private fun MangaSummary(
expanded: Boolean,
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 scrimHeight = with(LocalDensity.current) { remember { 24.sp.roundToPx() } }
SubcomposeLayout(modifier = modifier.clipToBounds()) { constraints ->
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 {
Layout(
modifier = modifier.clipToBounds(),
contents = listOf(
{
Text(
text = if (expanded) expandedDescription else shrunkDescription,
maxLines = Int.MAX_VALUE,
text = "\n\n", // Shows at least 3 lines
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 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())),
)
}
}.map { it.measure(Constraints.fixed(width = constraints.maxWidth, height = scrimHeight)) }
val actualPlaceable = actual.single()
.measure(constraints)
val scrimPlaceable = scrim.single()
.measure(Constraints.fixed(width = constraints.maxWidth, height = scrimHeight))
val currentHeight = shrunkHeight + ((heightDelta + scrimHeight) * animProgress).roundToInt()
layout(constraints.maxWidth, currentHeight) {
actualPlaceable.forEach {
it.place(0, 0)
}
actualPlaceable.place(0, 0)
val scrimY = currentHeight - scrimHeight
scrimPlaceable.forEach {
it.place(0, scrimY)
}
scrimPlaceable.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.windowInsetsPadding
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.GetApp
import androidx.compose.material.icons.outlined.HelpOutline
@ -128,7 +130,7 @@ fun MoreScreen(
item {
TextPreferenceWidget(
title = stringResource(R.string.categories),
icon = Icons.Outlined.Label,
icon = Icons.AutoMirrored.Outlined.Label,
onPreferenceClick = onClickCategories,
)
}
@ -166,7 +168,7 @@ fun MoreScreen(
item {
TextPreferenceWidget(
title = stringResource(R.string.label_help),
icon = Icons.Outlined.HelpOutline,
icon = Icons.AutoMirrored.Outlined.HelpOutline,
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.width
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.outlined.NewReleases
import androidx.compose.material3.Icon
@ -60,7 +61,7 @@ fun NewUpdateScreen(
) {
Text(text = stringResource(R.string.update_check_open))
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.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.ui.res.stringResource
import eu.kanade.presentation.components.UpIcon
import eu.kanade.presentation.components.AppBar
import tachiyomi.presentation.core.components.material.Scaffold
@Composable
@ -19,15 +16,9 @@ fun PreferenceScaffold(
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text(text = stringResource(titleRes)) },
navigationIcon = {
if (onBackPressed != null) {
IconButton(onClick = onBackPressed) {
UpIcon()
}
}
},
AppBar(
title = stringResource(titleRes),
navigateUp = onBackPressed,
actions = actions,
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.download.DownloadCache
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.NetworkPreferences
import eu.kanade.tachiyomi.network.PREF_DOH_360
@ -328,7 +327,6 @@ object SettingsAdvancedScreen : SearchableSettings {
private fun getLibraryGroup(): Preference.PreferenceGroup {
val scope = rememberCoroutineScope()
val context = LocalContext.current
val trackerManager = remember { Injekt.get<TrackerManager>() }
return Preference.PreferenceGroup(
title = stringResource(R.string.label_library),
@ -337,12 +335,6 @@ object SettingsAdvancedScreen : SearchableSettings {
title = stringResource(R.string.pref_refresh_library_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(
title = stringResource(R.string.pref_reset_viewer_flags),
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.result.contract.ActivityResultContracts
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
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.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@ -39,11 +32,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import com.hippo.unifile.UniFile
import eu.kanade.presentation.extensions.RequestStoragePermission
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.permissions.PermissionRequestHelper
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupConst
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.service.GoogleDriveService
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.copyToClipboard
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.launch
import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.domain.sync.SyncPreferences
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.util.collectAsState
@ -79,7 +71,7 @@ object SettingsBackupAndSyncScreen : SearchableSettings {
override fun getPreferences(): List<Preference> {
val backupPreferences = Injekt.get<BackupPreferences>()
DiskUtil.RequestStoragePermission()
PermissionRequestHelper.requestStoragePermission()
val syncPreferences = remember { Injekt.get<SyncPreferences>() }
val syncService by syncPreferences.syncService().collectAsState()
@ -263,22 +255,23 @@ object SettingsBackupAndSyncScreen : SearchableSettings {
val state = rememberLazyListState()
ScrollbarLazyColumn(state = state) {
item {
CreateBackupDialogItem(
isSelected = true,
title = stringResource(R.string.manga),
LabeledCheckbox(
label = stringResource(R.string.manga),
checked = true,
onCheckedChange = {},
)
}
choices.forEach { (k, v) ->
item {
val isSelected = flags.contains(k)
CreateBackupDialogItem(
isSelected = isSelected,
title = stringResource(v),
modifier = Modifier.clickable {
if (isSelected) {
flags.remove(k)
} else {
LabeledCheckbox(
label = stringResource(v),
checked = isSelected,
onCheckedChange = {
if (it) {
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
private fun getRestoreBackupPref(): Preference.PreferenceItem.TextPreference {
val context = LocalContext.current
@ -341,7 +311,7 @@ object SettingsBackupAndSyncScreen : SearchableSettings {
AlertDialog(
onDismissRequest = onDismissRequest,
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 = {
TextButton(
onClick = {
@ -349,7 +319,7 @@ object SettingsBackupAndSyncScreen : SearchableSettings {
onDismissRequest()
},
) {
Text(text = stringResource(android.R.string.copy))
Text(text = stringResource(R.string.action_copy_to_clipboard))
}
},
confirmButton = {
@ -413,21 +383,24 @@ object SettingsBackupAndSyncScreen : SearchableSettings {
}
},
) {
if (it != null) {
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)
if (it == null) {
error = InvalidRestore(message = context.getString(R.string.file_null_uri_error))
return@rememberLauncherForActivityResult
}
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(
@ -649,6 +622,6 @@ private data class MissingRestoreComponents(
)
private data class InvalidRestore(
val uri: Uri,
val uri: Uri? = null,
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.tachiyomi.R
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.ui.category.CategoryScreen
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@ -197,12 +196,6 @@ object SettingsLibraryScreen : SearchableSettings {
title = stringResource(R.string.pref_library_update_refresh_metadata),
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(
pref = libraryPreferences.autoUpdateMangaRestrictions(),
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.shape.RoundedCornerShape
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.Code
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.SettingsBackupRestore
import androidx.compose.material.icons.outlined.Sync
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
@ -44,7 +42,6 @@ import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.components.AppBar
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.widget.TextPreferenceWidget
import eu.kanade.presentation.util.LocalBackPress
@ -82,21 +79,13 @@ object SettingsMainScreen : Screen() {
val backPress = LocalBackPress.currentOrThrow
val containerColor = if (twoPane) getPalerSurface() else MaterialTheme.colorScheme.surface
val topBarState = rememberTopAppBarState()
Scaffold(
topBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(topBarState),
topBar = { scrollBehavior ->
TopAppBar(
title = {
Text(
text = stringResource(R.string.label_settings),
modifier = Modifier.padding(start = 8.dp),
)
},
navigationIcon = {
IconButton(onClick = backPress::invoke) {
UpIcon()
}
},
AppBar(
title = stringResource(R.string.label_settings),
navigateUp = backPress::invoke,
actions = {
AppBarActions(
listOf(
@ -198,7 +187,7 @@ object SettingsMainScreen : Screen() {
Item(
titleRes = R.string.pref_category_reader,
subtitleRes = R.string.pref_reader_summary,
icon = Icons.Outlined.ChromeReaderMode,
icon = Icons.AutoMirrored.Outlined.ChromeReaderMode,
screen = SettingsReaderScreen,
),
Item(

View File

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

View File

@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardOptions
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.VisibilityOff
import androidx.compose.material.icons.outlined.Close
@ -72,7 +73,7 @@ object SettingsTrackingScreen : SearchableSettings {
val uriHandler = LocalUriHandler.current
IconButton(onClick = { uriHandler.openUri("https://tachiyomi.org/docs/guides/tracking") }) {
Icon(
imageVector = Icons.Outlined.HelpOutline,
imageVector = Icons.AutoMirrored.Outlined.HelpOutline,
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.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Public
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.text.HtmlCompat
@ -41,19 +35,9 @@ class OpenSourceLibraryLicenseScreen(
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
text = name,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
navigationIcon = {
IconButton(onClick = navigator::pop) {
Icon(imageVector = Icons.Default.ArrowBack, contentDescription = null)
}
},
AppBar(
title = name,
navigateUp = navigator::pop,
actions = {
if (!website.isNullOrEmpty()) {
AppBarActions(

View File

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

View File

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

View File

@ -1,7 +1,6 @@
package eu.kanade.presentation.more.settings.screen.debug
import android.os.Build
import android.webkit.WebView
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
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.tachiyomi.R
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.WebViewUtil
import kotlinx.coroutines.guava.await
class DebugInfoScreen : Screen() {
@ -68,15 +68,7 @@ class DebugInfoScreen : Screen() {
@Composable
@ReadOnlyComposable
private fun getWebViewVersion(): String {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
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"
}
return WebViewUtil.getVersion(LocalContext.current)
}
@Composable

View File

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

View File

@ -1,30 +1,20 @@
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.selection.selectable
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
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.unit.dp
import androidx.compose.ui.window.DialogProperties
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.components.LabeledCheckbox
@Composable
fun MultiSelectListPreferenceWidget(
@ -55,33 +45,17 @@ fun MultiSelectListPreferenceWidget(
preference.entries.forEach { current ->
item {
val isSelected = selected.contains(current.key)
val onSelectionChanged = {
when (!isSelected) {
true -> selected.add(current.key)
false -> selected.remove(current.key)
}
}
Row(
verticalAlignment = Alignment.CenterVertically,
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),
)
}
LabeledCheckbox(
label = current.value,
checked = isSelected,
onCheckedChange = {
if (it) {
selected.add(current.key)
} else {
selected.remove(current.key)
}
},
)
}
}
}

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.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
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.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@ -24,6 +22,7 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
@Composable
fun BottomReaderBar(
backgroundColor: Color,
readingMode: ReadingModeType,
onClickReadingMode: () -> Unit,
orientationMode: OrientationType,
@ -32,11 +31,6 @@ fun BottomReaderBar(
onClickCropBorder: () -> 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(
modifier = Modifier
.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.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.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import eu.kanade.domain.track.model.toDbTrack
import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.presentation.track.components.TrackLogoIcon
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.ui.manga.track.TrackItem
import eu.kanade.tachiyomi.util.system.copyToClipboard
import tachiyomi.presentation.core.util.ThemePreviews
import java.text.DateFormat
private const val UnsetStatusTextAlpha = 0.5F
@ -168,6 +171,7 @@ private fun TrackInfoItem(
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
)
}
VerticalDivider()
@ -254,6 +258,7 @@ private fun TrackDetailsItem(
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyMedium,
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.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.components.WheelNumberPicker
import tachiyomi.presentation.core.components.WheelTextPicker
import tachiyomi.presentation.core.components.material.AlertDialogContent
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.util.ThemePreviews
import tachiyomi.presentation.core.util.isScrolledToEnd
import tachiyomi.presentation.core.util.isScrolledToStart
@ -171,7 +173,7 @@ fun TrackDateSelector(
Spacer(modifier = Modifier.weight(1f))
}
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel))
Text(text = stringResource(R.string.action_cancel))
}
TextButton(onClick = { onConfirm(pickerState.selectedDateMillis!!) }) {
Text(text = stringResource(R.string.action_ok))
@ -209,7 +211,7 @@ private fun BaseSelector(
Spacer(modifier = Modifier.weight(1f))
}
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel))
Text(text = stringResource(R.string.action_cancel))
}
TextButton(onClick = onConfirm) {
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.KeyboardOptions
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.CheckCircle
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.style.TextOverflow
import androidx.compose.ui.text.toLowerCase
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.manga.components.MangaCover
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.model.TrackSearch
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.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen
import tachiyomi.presentation.core.util.ThemePreviews
import tachiyomi.presentation.core.util.plus
import tachiyomi.presentation.core.util.runOnEnterKeyPressed
import tachiyomi.presentation.core.util.secondaryItemAlpha
@ -94,7 +98,7 @@ fun TrackerSearch(
navigationIcon = {
IconButton(onClick = onDismissRequest) {
Icon(
imageVector = Icons.Default.ArrowBack,
imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
contentDescription = null,
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.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.data.track.Tracker
import tachiyomi.presentation.core.util.ThemePreviews
import tachiyomi.presentation.core.util.clickableNoIndication
@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.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
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.ArrowForward
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -22,8 +27,10 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.google.accompanist.web.AccompanistWebViewClient
import com.google.accompanist.web.LoadingState
import com.google.accompanist.web.WebView
@ -72,7 +79,7 @@ fun WebViewScreenContent(
super.onPageFinished(view, url)
scope.launch {
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(
topBar = {
Box {
AppBar(
title = state.pageTitle ?: initialTitle,
subtitle = currentUrl,
navigateUp = onNavigateUp,
navigationIcon = Icons.Outlined.Close,
actions = {
AppBarActions(
listOf(
AppBar.Action(
title = stringResource(R.string.action_webview_back),
icon = Icons.Outlined.ArrowBack,
onClick = {
if (navigator.canGoBack) {
navigator.navigateBack()
}
Column {
AppBar(
title = state.pageTitle ?: initialTitle,
subtitle = currentUrl,
navigateUp = onNavigateUp,
navigationIcon = Icons.Outlined.Close,
actions = {
AppBarActions(
listOf(
AppBar.Action(
title = stringResource(R.string.action_webview_back),
icon = Icons.AutoMirrored.Outlined.ArrowBack,
onClick = {
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) {
is LoadingState.Initializing -> LinearProgressIndicator(
modifier = Modifier
@ -158,7 +182,7 @@ fun WebViewScreenContent(
.align(Alignment.BottomCenter),
)
is LoadingState.Loading -> LinearProgressIndicator(
progress = (loadingState as? LoadingState.Loading)?.progress ?: 1f,
progress = { (loadingState as? LoadingState.Loading)?.progress ?: 1f },
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter),
@ -168,38 +192,27 @@ fun WebViewScreenContent(
}
},
) { contentPadding ->
Column(
modifier = Modifier.padding(contentPadding),
) {
if (showCloudflareHelp) {
WarningBanner(
textRes = R.string.information_cloudflare_help,
modifier = Modifier.clickable {
uriHandler.openUri("https://tachiyomi.org/docs/guides/troubleshooting/#cloudflare")
},
)
}
WebView(
state = state,
modifier = Modifier
.fillMaxSize()
.padding(contentPadding),
navigator = navigator,
onCreated = { webView ->
webView.setDefaultSettings()
WebView(
state = state,
modifier = Modifier.weight(1f),
navigator = navigator,
onCreated = { webView ->
webView.setDefaultSettings()
// Debug mode (chrome://inspect/#devices)
if (BuildConfig.DEBUG &&
0 != webView.context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE
) {
WebView.setWebContentsDebuggingEnabled(true)
}
// Debug mode (chrome://inspect/#devices)
if (BuildConfig.DEBUG &&
0 != webView.context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE
) {
WebView.setWebContentsDebuggingEnabled(true)
}
headers["user-agent"]?.let {
webView.settings.userAgentString = it
}
},
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> {
if (flags and BACKUP_SOURCE_PREFS_MASK != BACKUP_SOURCE_PREFS) return emptyList()
return sourceManager.getOnlineSources()
return sourceManager.getCatalogueSources()
.filterIsInstance<ConfigurableSource>()
.map {
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.StringPreferenceValue
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.sourcePreferences
import eu.kanade.tachiyomi.util.BackupUtil
@ -589,6 +590,9 @@ class BackupRestorer(
private fun restoreAppPreferences(preferences: List<BackupPreference>) {
restorePreferences(preferences, preferenceStore)
LibraryUpdateJob.setupTask(context)
BackupCreateJob.setupTask(context)
restoreProgress += 1
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.model.copyFrom
import eu.kanade.domain.manga.model.toSManga
import eu.kanade.domain.track.interactor.RefreshTracks
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.CoverCache
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 getCategories: GetCategories = Injekt.get()
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get()
private val refreshTracks: RefreshTracks = Injekt.get()
private val fetchInterval: FetchInterval = Injekt.get()
private val notifier = LibraryUpdateNotifier(context)
@ -131,7 +129,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
when (target) {
Target.CHAPTERS -> updateChapterList()
Target.COVERS -> updateCovers()
Target.TRACKING -> updateTrackings()
}
Result.success()
} catch (e: Exception) {
@ -304,10 +301,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
}
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()
}
/**
* 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(
updatingManga: CopyOnWriteArrayList<Manga>,
completed: AtomicInteger,
@ -500,7 +466,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
enum class Target {
CHAPTERS, // Manga chapters
COVERS, // Manga covers
TRACKING, // Tracking metadata
}
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
import android.app.Application
import androidx.annotation.CallSuper
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
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.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 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 networkService: NetworkHelper by injectLazy()
private val insertTrack: InsertTrack by injectLazy()
private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack by injectLazy()
val id: Long
open val client: OkHttpClient
get() = networkService.client
val name: String
val client: OkHttpClient
// Application and remote support for reading dates
open val supportsReadingDates: Boolean = false
@DrawableRes
abstract fun getLogo(): Int
val supportsReadingDates: Boolean
@ColorInt
abstract fun getLogoColor(): Int
fun getLogoColor(): Int
abstract fun getStatusList(): List<Int>
@DrawableRes
fun getLogo(): Int
fun getStatusList(): List<Int>
@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?
open fun get10PointScore(track: DomainTrack): Double {
return track.score
}
fun get10PointScore(track: tachiyomi.domain.track.model.Track): Double
open fun indexToScore(index: Int): Float {
return index.toFloat()
}
fun indexToScore(index: Int): Float
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
open fun logout() {
trackPreferences.setCredentials(this, "", "")
}
fun logout()
open val isLoggedIn: Boolean
get() = getUsername().isNotEmpty() &&
getPassword().isNotEmpty()
val isLoggedIn: Boolean
fun getUsername() = trackPreferences.trackUsername(this).get()
fun getUsername(): String
fun getPassword() = trackPreferences.trackPassword(this).get()
fun getPassword(): String
fun saveCredentials(username: String, password: String) {
trackPreferences.setCredentials(this, username, password)
}
fun saveCredentials(username: String, password: String)
// TODO: move this to an interactor, and update all trackers based on common data
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)
suspend fun register(item: Track, mangaId: Long)
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]?
// Update chapter progress if newer chapters marked read locally
if (hasReadChapters) {
val latestLocalReadChapterNumber = allChapters
.sortedBy { it.chapterNumber }
.takeWhile { it.read }
.lastOrNull()
?.chapterNumber ?: -1.0
suspend fun setRemoteScore(track: Track, scoreString: String)
if (latestLocalReadChapterNumber > track.lastChapterRead) {
track = track.copy(
lastChapterRead = latestLocalReadChapterNumber,
)
setRemoteLastChapterRead(track.toDbTrack(), latestLocalReadChapterNumber.toInt())
}
suspend fun setRemoteStartDate(track: Track, epochMillis: Long)
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@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) }
}
}
}
suspend fun setRemoteFinishDate(track: Track, epochMillis: Long)
}

View File

@ -4,15 +4,15 @@ import android.graphics.Color
import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R
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.Tracker
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import uy.kohesive.injekt.injectLazy
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 {
const val READING = 1

View File

@ -4,13 +4,13 @@ import android.graphics.Color
import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R
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 kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
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()

View File

@ -4,8 +4,8 @@ import android.graphics.Color
import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R
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.Tracker
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.Source
@ -16,7 +16,7 @@ import uy.kohesive.injekt.injectLazy
import java.security.MessageDigest
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 {
const val UNREAD = 1

View File

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

View File

@ -4,8 +4,8 @@ import android.graphics.Color
import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R
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.Tracker
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.source.Source
import okhttp3.Dns
@ -13,7 +13,7 @@ import okhttp3.OkHttpClient
import tachiyomi.domain.manga.model.Manga
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 {
const val UNREAD = 1

View File

@ -4,13 +4,13 @@ import android.graphics.Color
import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R
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.Tracker
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.copyTo
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.toTrackSearch
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 {
const val READING_LIST = 0

View File

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

View File

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

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.data.track.shikimori
import android.net.Uri
import androidx.core.net.toUri
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.model.TrackSearch
@ -37,12 +38,12 @@ class ShikimoriApi(
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 {
with(json) {
val payload = buildJsonObject {
putJsonObject("user_rate") {
put("user_id", user_id)
put("user_id", userId)
put("target_id", track.media_id)
put("target_type", "Manga")
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 {
return withIOContext {
@ -194,14 +195,14 @@ class ShikimoriApi(
private const val clientId = "1aaf4cf232372708e98b5abc813d795b539c5a916dbbfe9ac61bf02a360832cc"
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 oauthUrl = "$baseUrl/oauth/token"
private const val loginUrl = "$baseUrl/oauth/authorize"
private const val redirectUrl = "tachiyomi://shikimori-auth"
fun authUrl() = loginUrl.toUri().buildUpon()
fun authUrl(): Uri = loginUrl.toUri().buildUpon()
.appendQueryParameter("client_id", clientId)
.appendQueryParameter("redirect_uri", redirectUrl)
.appendQueryParameter("response_type", "code")

View File

@ -4,14 +4,14 @@ import android.graphics.Color
import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R
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.Tracker
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.source.Source
import tachiyomi.domain.manga.model.Manga as DomainManga
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) }

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.SourceFactory
import eu.kanade.tachiyomi.util.lang.Hash
import eu.kanade.tachiyomi.util.storage.copyAndSetReadOnlyTo
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
@ -97,7 +98,8 @@ internal object ExtensionLoader {
val target = File(getPrivateExtensionDir(context), "${extension.packageName}.$PRIVATE_EXTENSION_EXTENSION")
return try {
file.copyTo(target, overwrite = true)
target.delete()
file.copyAndSetReadOnlyTo(target, overwrite = true)
if (currentExtension != null) {
ExtensionInstallReceiver.notifyReplaced(context, extension.packageName)
} 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.TabOptions
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.tachiyomi.R
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.sourcesTab
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.storage.DiskUtil
data class BrowseTab(
private val toExtensions: Boolean = false,
@ -66,7 +65,7 @@ data class BrowseTab(
)
// For local source
DiskUtil.RequestStoragePermission()
PermissionRequestHelper.requestStoragePermission()
LaunchedEffect(Unit) {
(context as? MainActivity)?.ready = true

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.browse.migration.search
import androidx.compose.runtime.Immutable
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.launch
import tachiyomi.domain.manga.interactor.GetManga
@ -16,7 +16,7 @@ class MigrateSearchScreenDialogScreenModel(
) : StateScreenModel<MigrateSearchScreenDialogScreenModel.State>(State()) {
init {
coroutineScope.launch {
screenModelScope.launch {
val manga = getManga.await(mangaId)!!
mutableState.update {

View File

@ -1,6 +1,6 @@
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.ui.browse.source.globalsearch.SearchScreenModel
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
@ -16,7 +16,7 @@ class MigrateSearchScreenModel(
) : SearchScreenModel() {
init {
coroutineScope.launch {
screenModelScope.launch {
val manga = getManga.await(mangaId)!!
mutableState.update {
it.copy(

View File

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

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.browse.migration.sources
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
@ -29,7 +30,7 @@ fun Screen.migrateSourceTab(): TabContent {
actions = listOf(
AppBar.Action(
title = stringResource(R.string.migration_help_guide),
icon = Icons.Outlined.HelpOutline,
icon = Icons.AutoMirrored.Outlined.HelpOutline,
onClick = {
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 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.ToggleLanguage
import eu.kanade.domain.source.interactor.ToggleSource
@ -25,7 +25,7 @@ class SourcesFilterScreenModel(
) : StateScreenModel<SourcesFilterScreenModel.State>(State.Loading) {
init {
coroutineScope.launch {
screenModelScope.launch {
combine(
getLanguagesWithSources.subscribe(),
preferences.enabledLanguages().changes(),

View File

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.browse.source
import androidx.compose.runtime.Immutable
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.ToggleSource
import eu.kanade.domain.source.interactor.ToggleSourcePin
@ -31,7 +31,7 @@ class SourcesScreenModel(
val events = _events.receiveAsFlow()
init {
coroutineScope.launchIO {
screenModelScope.launchIO {
getEnabledSources.subscribe()
.catch {
logcat(LogPriority.ERROR, it)

View File

@ -12,7 +12,7 @@ import androidx.paging.cachedIn
import androidx.paging.filter
import androidx.paging.map
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.domain.base.BasePreferences
import eu.kanade.domain.manga.interactor.UpdateManga
@ -72,7 +72,7 @@ class BrowseSourceScreenModel(
private val addTracks: AddTracks = Injekt.get(),
) : 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)
@ -220,7 +220,7 @@ class BrowseSourceScreenModel(
* @param manga the manga to update.
*/
fun changeMangaFavorite(manga: Manga) {
coroutineScope.launch {
screenModelScope.launch {
var new = manga.copy(
favorite = !manga.favorite,
dateAdded = when (manga.favorite) {
@ -241,7 +241,7 @@ class BrowseSourceScreenModel(
}
fun addFavorite(manga: Manga) {
coroutineScope.launch {
screenModelScope.launch {
val categories = getCategories()
val defaultCategoryId = libraryPreferences.defaultCategory().get()
val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() }
@ -291,7 +291,7 @@ class BrowseSourceScreenModel(
}
fun moveMangaToCategories(manga: Manga, categoryIds: List<Long>) {
coroutineScope.launchIO {
screenModelScope.launchIO {
setMangaCategories.await(
mangaId = manga.id,
categoryIds = categoryIds.toList(),

View File

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

View File

@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import cafe.adriel.voyager.core.model.rememberScreenModel
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.ui.browse.source.globalsearch.GlobalSearchScreen
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.screens.LoadingScreen
@ -23,6 +25,7 @@ class DeepLinkScreen(
@Composable
override fun Content() {
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel {
@ -46,12 +49,22 @@ class DeepLinkScreen(
navigator.replace(GlobalSearchScreen(query))
}
is DeepLinkScreenModel.State.Result -> {
navigator.replace(
MangaScreen(
(state as DeepLinkScreenModel.State.Result).manga.id,
true,
),
)
val resultState = state as DeepLinkScreenModel.State.Result
if (resultState.chapterId == null) {
navigator.replace(
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 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.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.UriType
import kotlinx.coroutines.flow.update
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.source.service.SourceManager
import uy.kohesive.injekt.Injekt
@ -15,25 +25,59 @@ import uy.kohesive.injekt.api.get
class DeepLinkScreenModel(
query: String = "",
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) {
init {
coroutineScope.launchIO {
val manga = sourceManager.getCatalogueSources()
screenModelScope.launchIO {
val source = sourceManager.getCatalogueSources()
.filterIsInstance<ResolvableSource>()
.filter { it.canResolveUri(query) }
.firstNotNullOfOrNull { it.getManga(query)?.toDomainManga(it.id) }
.firstOrNull { it.getUriType(query) != UriType.Unknown }
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 {
if (manga == null) {
State.NoResults
} 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 {
@Immutable
data object Loading : State
@ -42,6 +86,6 @@ class DeepLinkScreenModel(
data object NoResults : State
@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.padding
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.outlined.Pause
import androidx.compose.material.icons.outlined.Sort
@ -185,7 +186,7 @@ object DownloadQueueScreen : Screen() {
listOf(
AppBar.Action(
title = stringResource(R.string.action_sort),
icon = Icons.Outlined.Sort,
icon = Icons.AutoMirrored.Outlined.Sort,
onClick = { sortExpanded = true },
),
AppBar.OverflowAction(

View File

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

View File

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

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