mirror of
https://github.com/mihonapp/mihon.git
synced 2024-11-15 15:02:49 +01:00
Merge branch 'master' into sync-part-final
This commit is contained in:
commit
2e5686372a
@ -22,7 +22,7 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "eu.kanade.tachiyomi"
|
applicationId = "eu.kanade.tachiyomi"
|
||||||
|
|
||||||
versionCode = 113
|
versionCode = 114
|
||||||
versionName = "0.14.7"
|
versionName = "0.14.7"
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||||
|
@ -11,8 +11,11 @@ import eu.kanade.domain.manga.interactor.GetExcludedScanlators
|
|||||||
import eu.kanade.domain.manga.interactor.SetExcludedScanlators
|
import eu.kanade.domain.manga.interactor.SetExcludedScanlators
|
||||||
import eu.kanade.domain.manga.interactor.SetMangaViewerFlags
|
import eu.kanade.domain.manga.interactor.SetMangaViewerFlags
|
||||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||||
|
import eu.kanade.domain.source.interactor.CreateSourceRepo
|
||||||
|
import eu.kanade.domain.source.interactor.DeleteSourceRepo
|
||||||
import eu.kanade.domain.source.interactor.GetEnabledSources
|
import eu.kanade.domain.source.interactor.GetEnabledSources
|
||||||
import eu.kanade.domain.source.interactor.GetLanguagesWithSources
|
import eu.kanade.domain.source.interactor.GetLanguagesWithSources
|
||||||
|
import eu.kanade.domain.source.interactor.GetSourceRepos
|
||||||
import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount
|
import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount
|
||||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
||||||
import eu.kanade.domain.source.interactor.ToggleLanguage
|
import eu.kanade.domain.source.interactor.ToggleLanguage
|
||||||
@ -167,5 +170,9 @@ class DomainModule : InjektModule {
|
|||||||
addFactory { ToggleLanguage(get()) }
|
addFactory { ToggleLanguage(get()) }
|
||||||
addFactory { ToggleSource(get()) }
|
addFactory { ToggleSource(get()) }
|
||||||
addFactory { ToggleSourcePin(get()) }
|
addFactory { ToggleSourcePin(get()) }
|
||||||
|
|
||||||
|
addFactory { CreateSourceRepo(get()) }
|
||||||
|
addFactory { DeleteSourceRepo(get()) }
|
||||||
|
addFactory { GetSourceRepos(get()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
package eu.kanade.domain.source.interactor
|
||||||
|
|
||||||
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
|
import tachiyomi.core.preference.plusAssign
|
||||||
|
|
||||||
|
class CreateSourceRepo(private val preferences: SourcePreferences) {
|
||||||
|
|
||||||
|
fun await(name: String): Result {
|
||||||
|
// Do not allow invalid formats
|
||||||
|
if (!name.matches(repoRegex) || name.startsWith(OFFICIAL_REPO_BASE_URL)) {
|
||||||
|
return Result.InvalidUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
preferences.extensionRepos() += name.substringBeforeLast("/index.min.json")
|
||||||
|
|
||||||
|
return Result.Success
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface Result {
|
||||||
|
data object InvalidUrl : Result
|
||||||
|
data object Success : Result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const val OFFICIAL_REPO_BASE_URL = "https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo"
|
||||||
|
private val repoRegex = """^https://.*/index\.min\.json$""".toRegex()
|
@ -0,0 +1,11 @@
|
|||||||
|
package eu.kanade.domain.source.interactor
|
||||||
|
|
||||||
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
|
import tachiyomi.core.preference.minusAssign
|
||||||
|
|
||||||
|
class DeleteSourceRepo(private val preferences: SourcePreferences) {
|
||||||
|
|
||||||
|
fun await(repo: String) {
|
||||||
|
preferences.extensionRepos() -= repo
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
package eu.kanade.domain.source.interactor
|
||||||
|
|
||||||
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
class GetSourceRepos(private val preferences: SourcePreferences) {
|
||||||
|
|
||||||
|
fun subscribe(): Flow<List<String>> {
|
||||||
|
return preferences.extensionRepos().changes()
|
||||||
|
.map { it.sortedWith(String.CASE_INSENSITIVE_ORDER) }
|
||||||
|
}
|
||||||
|
}
|
@ -38,6 +38,8 @@ class SourcePreferences(
|
|||||||
SetMigrateSorting.Direction.ASCENDING,
|
SetMigrateSorting.Direction.ASCENDING,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun extensionRepos() = preferenceStore.getStringSet("extension_repos", emptySet())
|
||||||
|
|
||||||
fun extensionUpdatesCount() = preferenceStore.getInt("ext_updates_count", 0)
|
fun extensionUpdatesCount() = preferenceStore.getInt("ext_updates_count", 0)
|
||||||
|
|
||||||
fun trustedSignatures() = preferenceStore.getStringSet(Preference.appStateKey("trusted_signatures"), emptySet())
|
fun trustedSignatures() = preferenceStore.getStringSet(Preference.appStateKey("trusted_signatures"), emptySet())
|
||||||
|
@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
|
||||||
import androidx.compose.material.icons.outlined.History
|
import androidx.compose.material.icons.outlined.History
|
||||||
import androidx.compose.material.icons.outlined.Settings
|
import androidx.compose.material.icons.outlined.Settings
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
@ -38,6 +37,7 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
@ -67,7 +67,6 @@ fun ExtensionDetailsScreen(
|
|||||||
state: ExtensionDetailsScreenModel.State,
|
state: ExtensionDetailsScreenModel.State,
|
||||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
||||||
onClickWhatsNew: () -> Unit,
|
onClickWhatsNew: () -> Unit,
|
||||||
onClickReadme: () -> Unit,
|
|
||||||
onClickEnableAll: () -> Unit,
|
onClickEnableAll: () -> Unit,
|
||||||
onClickDisableAll: () -> Unit,
|
onClickDisableAll: () -> Unit,
|
||||||
onClickClearCookies: () -> Unit,
|
onClickClearCookies: () -> Unit,
|
||||||
@ -91,13 +90,6 @@ fun ExtensionDetailsScreen(
|
|||||||
onClick = onClickWhatsNew,
|
onClick = onClickWhatsNew,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
add(
|
|
||||||
AppBar.Action(
|
|
||||||
title = stringResource(MR.strings.action_faq_and_guides),
|
|
||||||
icon = Icons.AutoMirrored.Outlined.HelpOutline,
|
|
||||||
onClick = onClickReadme,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
addAll(
|
addAll(
|
||||||
listOf(
|
listOf(
|
||||||
@ -125,7 +117,7 @@ fun ExtensionDetailsScreen(
|
|||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
if (state.extension == null) {
|
if (state.extension == null) {
|
||||||
EmptyScreen(
|
EmptyScreen(
|
||||||
stringRes = MR.strings.empty_screen,
|
MR.strings.empty_screen,
|
||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
)
|
)
|
||||||
return@Scaffold
|
return@Scaffold
|
||||||
@ -158,6 +150,21 @@ private fun ExtensionDetails(
|
|||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
|
extension.isRepoSource ->
|
||||||
|
item {
|
||||||
|
val uriHandler = LocalUriHandler.current
|
||||||
|
WarningBanner(
|
||||||
|
MR.strings.repo_extension_message,
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
extension.repoUrl ?: return@clickable
|
||||||
|
uriHandler.openUri(
|
||||||
|
extension.repoUrl
|
||||||
|
.replace("https://raw.githubusercontent.com", "https://github.com")
|
||||||
|
.removeSuffix("/repo/"),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
extension.isUnofficial ->
|
extension.isUnofficial ->
|
||||||
item {
|
item {
|
||||||
WarningBanner(MR.strings.unofficial_extension_message)
|
WarningBanner(MR.strings.unofficial_extension_message)
|
||||||
|
@ -28,6 +28,7 @@ import androidx.compose.ui.focus.focusRequester
|
|||||||
import eu.kanade.core.preference.asToggleableState
|
import eu.kanade.core.preference.asToggleableState
|
||||||
import eu.kanade.presentation.category.visualName
|
import eu.kanade.presentation.category.visualName
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import tachiyomi.core.preference.CheckboxState
|
import tachiyomi.core.preference.CheckboxState
|
||||||
import tachiyomi.domain.category.model.Category
|
import tachiyomi.domain.category.model.Category
|
||||||
@ -40,12 +41,12 @@ import kotlin.time.Duration.Companion.seconds
|
|||||||
fun CategoryCreateDialog(
|
fun CategoryCreateDialog(
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onCreate: (String) -> Unit,
|
onCreate: (String) -> Unit,
|
||||||
categories: ImmutableList<Category>,
|
categories: ImmutableList<String>,
|
||||||
) {
|
) {
|
||||||
var name by remember { mutableStateOf("") }
|
var name by remember { mutableStateOf("") }
|
||||||
|
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val nameAlreadyExists = remember(name) { categories.anyWithName(name) }
|
val nameAlreadyExists = remember(name) { categories.contains(name) }
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
@ -70,10 +71,13 @@ fun CategoryCreateDialog(
|
|||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier = Modifier.focusRequester(focusRequester),
|
modifier = Modifier
|
||||||
|
.focusRequester(focusRequester),
|
||||||
value = name,
|
value = name,
|
||||||
onValueChange = { name = it },
|
onValueChange = { name = it },
|
||||||
label = { Text(text = stringResource(MR.strings.name)) },
|
label = {
|
||||||
|
Text(text = stringResource(MR.strings.name))
|
||||||
|
},
|
||||||
supportingText = {
|
supportingText = {
|
||||||
val msgRes = if (name.isNotEmpty() && nameAlreadyExists) {
|
val msgRes = if (name.isNotEmpty() && nameAlreadyExists) {
|
||||||
MR.strings.error_category_exists
|
MR.strings.error_category_exists
|
||||||
@ -99,14 +103,14 @@ fun CategoryCreateDialog(
|
|||||||
fun CategoryRenameDialog(
|
fun CategoryRenameDialog(
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onRename: (String) -> Unit,
|
onRename: (String) -> Unit,
|
||||||
categories: ImmutableList<Category>,
|
categories: ImmutableList<String>,
|
||||||
category: Category,
|
category: String,
|
||||||
) {
|
) {
|
||||||
var name by remember { mutableStateOf(category.name) }
|
var name by remember { mutableStateOf(category) }
|
||||||
var valueHasChanged by remember { mutableStateOf(false) }
|
var valueHasChanged by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val nameAlreadyExists = remember(name) { categories.anyWithName(name) }
|
val nameAlreadyExists = remember(name) { categories.contains(name) }
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
@ -163,7 +167,7 @@ fun CategoryRenameDialog(
|
|||||||
fun CategoryDeleteDialog(
|
fun CategoryDeleteDialog(
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onDelete: () -> Unit,
|
onDelete: () -> Unit,
|
||||||
category: Category,
|
category: String,
|
||||||
) {
|
) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
@ -184,7 +188,7 @@ fun CategoryDeleteDialog(
|
|||||||
Text(text = stringResource(MR.strings.delete_category))
|
Text(text = stringResource(MR.strings.delete_category))
|
||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
Text(text = stringResource(MR.strings.delete_category_confirmation, category.name))
|
Text(text = stringResource(MR.strings.delete_category_confirmation, category))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -220,7 +224,7 @@ fun CategorySortAlphabeticallyDialog(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChangeCategoryDialog(
|
fun ChangeCategoryDialog(
|
||||||
initialSelection: List<CheckboxState<Category>>,
|
initialSelection: ImmutableList<CheckboxState<Category>>,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onEditCategories: () -> Unit,
|
onEditCategories: () -> Unit,
|
||||||
onConfirm: (List<Long>, List<Long>) -> Unit,
|
onConfirm: (List<Long>, List<Long>) -> Unit,
|
||||||
@ -292,7 +296,7 @@ fun ChangeCategoryDialog(
|
|||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
val mutableList = selection.toMutableList()
|
val mutableList = selection.toMutableList()
|
||||||
mutableList[index] = it.next()
|
mutableList[index] = it.next()
|
||||||
selection = mutableList.toList()
|
selection = mutableList.toList().toImmutableList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
@ -326,7 +330,3 @@ fun ChangeCategoryDialog(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun List<Category>.anyWithName(name: String): Boolean {
|
|
||||||
return any { name == it.name }
|
|
||||||
}
|
|
||||||
|
@ -49,7 +49,7 @@ fun CategoryListItem(
|
|||||||
),
|
),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = "")
|
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = null)
|
||||||
Text(
|
Text(
|
||||||
text = category.name,
|
text = category.name,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -61,13 +61,13 @@ fun CategoryListItem(
|
|||||||
onClick = { onMoveUp(category) },
|
onClick = { onMoveUp(category) },
|
||||||
enabled = canMoveUp,
|
enabled = canMoveUp,
|
||||||
) {
|
) {
|
||||||
Icon(imageVector = Icons.Outlined.ArrowDropUp, contentDescription = "")
|
Icon(imageVector = Icons.Outlined.ArrowDropUp, contentDescription = null)
|
||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { onMoveDown(category) },
|
onClick = { onMoveDown(category) },
|
||||||
enabled = canMoveDown,
|
enabled = canMoveDown,
|
||||||
) {
|
) {
|
||||||
Icon(imageVector = Icons.Outlined.ArrowDropDown, contentDescription = "")
|
Icon(imageVector = Icons.Outlined.ArrowDropDown, contentDescription = null)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
IconButton(onClick = onRename) {
|
IconButton(onClick = onRename) {
|
||||||
|
@ -78,12 +78,13 @@ import tachiyomi.presentation.core.i18n.stringResource
|
|||||||
import tachiyomi.presentation.core.util.isScrolledToEnd
|
import tachiyomi.presentation.core.util.isScrolledToEnd
|
||||||
import tachiyomi.presentation.core.util.isScrollingUp
|
import tachiyomi.presentation.core.util.isScrollingUp
|
||||||
import tachiyomi.source.local.isLocal
|
import tachiyomi.source.local.isLocal
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MangaScreen(
|
fun MangaScreen(
|
||||||
state: MangaScreenModel.State.Success,
|
state: MangaScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
fetchInterval: Int?,
|
nextUpdate: Instant?,
|
||||||
isTabletUi: Boolean,
|
isTabletUi: Boolean,
|
||||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
@ -138,7 +139,7 @@ fun MangaScreen(
|
|||||||
MangaScreenSmallImpl(
|
MangaScreenSmallImpl(
|
||||||
state = state,
|
state = state,
|
||||||
snackbarHostState = snackbarHostState,
|
snackbarHostState = snackbarHostState,
|
||||||
fetchInterval = fetchInterval,
|
nextUpdate = nextUpdate,
|
||||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||||
onBackClicked = onBackClicked,
|
onBackClicked = onBackClicked,
|
||||||
@ -175,7 +176,7 @@ fun MangaScreen(
|
|||||||
snackbarHostState = snackbarHostState,
|
snackbarHostState = snackbarHostState,
|
||||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||||
fetchInterval = fetchInterval,
|
nextUpdate = nextUpdate,
|
||||||
onBackClicked = onBackClicked,
|
onBackClicked = onBackClicked,
|
||||||
onChapterClicked = onChapterClicked,
|
onChapterClicked = onChapterClicked,
|
||||||
onDownloadChapter = onDownloadChapter,
|
onDownloadChapter = onDownloadChapter,
|
||||||
@ -211,7 +212,7 @@ fun MangaScreen(
|
|||||||
private fun MangaScreenSmallImpl(
|
private fun MangaScreenSmallImpl(
|
||||||
state: MangaScreenModel.State.Success,
|
state: MangaScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
fetchInterval: Int?,
|
nextUpdate: Instant?,
|
||||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
onBackClicked: () -> Unit,
|
onBackClicked: () -> Unit,
|
||||||
@ -272,10 +273,7 @@ private fun MangaScreenSmallImpl(
|
|||||||
onBackClicked()
|
onBackClicked()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BackHandler(
|
BackHandler(onBack = internalOnBackPressed)
|
||||||
enabled = isAnySelected,
|
|
||||||
onBack = { onAllChapterSelected(false) },
|
|
||||||
)
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@ -402,7 +400,7 @@ private fun MangaScreenSmallImpl(
|
|||||||
MangaActionRow(
|
MangaActionRow(
|
||||||
favorite = state.manga.favorite,
|
favorite = state.manga.favorite,
|
||||||
trackingCount = state.trackingCount,
|
trackingCount = state.trackingCount,
|
||||||
fetchInterval = fetchInterval,
|
nextUpdate = nextUpdate,
|
||||||
isUserIntervalMode = state.manga.fetchInterval < 0,
|
isUserIntervalMode = state.manga.fetchInterval < 0,
|
||||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||||
onWebViewClicked = onWebViewClicked,
|
onWebViewClicked = onWebViewClicked,
|
||||||
@ -462,7 +460,7 @@ private fun MangaScreenSmallImpl(
|
|||||||
fun MangaScreenLargeImpl(
|
fun MangaScreenLargeImpl(
|
||||||
state: MangaScreenModel.State.Success,
|
state: MangaScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
fetchInterval: Int?,
|
nextUpdate: Instant?,
|
||||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
onBackClicked: () -> Unit,
|
onBackClicked: () -> Unit,
|
||||||
@ -529,10 +527,7 @@ fun MangaScreenLargeImpl(
|
|||||||
onBackClicked()
|
onBackClicked()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BackHandler(
|
BackHandler(onBack = internalOnBackPressed)
|
||||||
enabled = isAnySelected,
|
|
||||||
onBack = { onAllChapterSelected(false) },
|
|
||||||
)
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@ -641,7 +636,7 @@ fun MangaScreenLargeImpl(
|
|||||||
MangaActionRow(
|
MangaActionRow(
|
||||||
favorite = state.manga.favorite,
|
favorite = state.manga.favorite,
|
||||||
trackingCount = state.trackingCount,
|
trackingCount = state.trackingCount,
|
||||||
fetchInterval = fetchInterval,
|
nextUpdate = nextUpdate,
|
||||||
isUserIntervalMode = state.manga.fetchInterval < 0,
|
isUserIntervalMode = state.manga.fetchInterval < 0,
|
||||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||||
onWebViewClicked = onWebViewClicked,
|
onWebViewClicked = onWebViewClicked,
|
||||||
|
@ -3,10 +3,6 @@ package eu.kanade.presentation.manga.components
|
|||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.drawable.BitmapDrawable
|
import android.graphics.drawable.BitmapDrawable
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.activity.compose.PredictiveBackHandler
|
|
||||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
|
||||||
import androidx.compose.animation.core.animate
|
|
||||||
import androidx.compose.animation.core.tween
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@ -29,18 +25,15 @@ import androidx.compose.material3.SnackbarHostState
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.unit.DpOffset
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.util.lerp
|
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
@ -55,13 +48,11 @@ import eu.kanade.presentation.components.DropdownMenu
|
|||||||
import eu.kanade.presentation.manga.EditCoverAction
|
import eu.kanade.presentation.manga.EditCoverAction
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
|
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import soup.compose.material.motion.MotionConstants
|
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.util.clickableNoIndication
|
import tachiyomi.presentation.core.util.clickableNoIndication
|
||||||
import kotlin.coroutines.cancellation.CancellationException
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MangaCoverDialog(
|
fun MangaCoverDialog(
|
||||||
@ -160,32 +151,10 @@ fun MangaCoverDialog(
|
|||||||
val statusBarPaddingPx = with(LocalDensity.current) { contentPadding.calculateTopPadding().roundToPx() }
|
val statusBarPaddingPx = with(LocalDensity.current) { contentPadding.calculateTopPadding().roundToPx() }
|
||||||
val bottomPaddingPx = with(LocalDensity.current) { contentPadding.calculateBottomPadding().roundToPx() }
|
val bottomPaddingPx = with(LocalDensity.current) { contentPadding.calculateBottomPadding().roundToPx() }
|
||||||
|
|
||||||
var scale by remember { mutableFloatStateOf(1f) }
|
|
||||||
PredictiveBackHandler { progress ->
|
|
||||||
try {
|
|
||||||
progress.collect { backEvent ->
|
|
||||||
scale = lerp(1f, 0.8f, LinearOutSlowInEasing.transform(backEvent.progress))
|
|
||||||
}
|
|
||||||
onDismissRequest()
|
|
||||||
} catch (e: CancellationException) {
|
|
||||||
animate(
|
|
||||||
initialValue = scale,
|
|
||||||
targetValue = 1f,
|
|
||||||
animationSpec = tween(durationMillis = MotionConstants.DefaultMotionDuration),
|
|
||||||
) { value, _ ->
|
|
||||||
scale = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.clickableNoIndication(onClick = onDismissRequest)
|
.clickableNoIndication(onClick = onDismissRequest),
|
||||||
.graphicsLayer {
|
|
||||||
scaleX = scale
|
|
||||||
scaleY = scale
|
|
||||||
},
|
|
||||||
) {
|
) {
|
||||||
AndroidView(
|
AndroidView(
|
||||||
factory = {
|
factory = {
|
||||||
|
@ -2,8 +2,11 @@ package eu.kanade.presentation.manga.components
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@ -16,10 +19,13 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||||
|
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import tachiyomi.domain.manga.interactor.FetchInterval
|
import tachiyomi.domain.manga.interactor.FetchInterval
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.WheelTextPicker
|
import tachiyomi.presentation.core.components.WheelTextPicker
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.i18n.pluralStringResource
|
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@ -59,37 +65,50 @@ fun DeleteChaptersDialog(
|
|||||||
@Composable
|
@Composable
|
||||||
fun SetIntervalDialog(
|
fun SetIntervalDialog(
|
||||||
interval: Int,
|
interval: Int,
|
||||||
nextUpdate: Long,
|
nextUpdate: Instant?,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onValueChanged: (Int) -> Unit,
|
onValueChanged: ((Int) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
var selectedInterval by rememberSaveable { mutableIntStateOf(if (interval < 0) -interval else 0) }
|
var selectedInterval by rememberSaveable { mutableIntStateOf(if (interval < 0) -interval else 0) }
|
||||||
|
|
||||||
val nextUpdateDays = remember(nextUpdate) {
|
val nextUpdateDays = remember(nextUpdate) {
|
||||||
|
return@remember if (nextUpdate != null) {
|
||||||
val now = Instant.now()
|
val now = Instant.now()
|
||||||
val nextUpdateInstant = Instant.ofEpochMilli(nextUpdate)
|
now.until(nextUpdate, ChronoUnit.DAYS).toInt().coerceAtLeast(0)
|
||||||
|
} else {
|
||||||
now.until(nextUpdateInstant, ChronoUnit.DAYS)
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
title = { Text(stringResource(MR.strings.manga_modify_calculated_interval_title)) },
|
title = { Text(stringResource(MR.strings.pref_library_update_smart_update)) },
|
||||||
text = {
|
text = {
|
||||||
Column {
|
Column {
|
||||||
if (nextUpdateDays >= 0) {
|
if (nextUpdateDays != null && nextUpdateDays >= 0) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(
|
stringResource(
|
||||||
MR.strings.manga_interval_expected_update,
|
MR.strings.manga_interval_expected_update,
|
||||||
pluralStringResource(
|
pluralStringResource(
|
||||||
MR.plurals.day,
|
MR.plurals.day,
|
||||||
count = nextUpdateDays.toInt(),
|
count = nextUpdateDays,
|
||||||
nextUpdateDays,
|
nextUpdateDays,
|
||||||
),
|
),
|
||||||
|
pluralStringResource(
|
||||||
|
MR.plurals.day,
|
||||||
|
count = interval,
|
||||||
|
interval,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(MaterialTheme.padding.small))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: selecting "1" then doesn't allow for future changes unless defaulting first?
|
||||||
|
if (onValueChanged != null && (isDevFlavor || isPreviewBuildType)) {
|
||||||
|
Text(stringResource(MR.strings.manga_interval_custom_amount))
|
||||||
|
|
||||||
BoxWithConstraints(
|
BoxWithConstraints(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
@ -112,6 +131,7 @@ fun SetIntervalDialog(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = onDismissRequest) {
|
TextButton(onClick = onDismissRequest) {
|
||||||
@ -120,7 +140,7 @@ fun SetIntervalDialog(
|
|||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
onValueChanged(selectedInterval)
|
onValueChanged?.invoke(selectedInterval)
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
}) {
|
}) {
|
||||||
Text(text = stringResource(MR.strings.action_ok))
|
Text(text = stringResource(MR.strings.action_ok))
|
||||||
|
@ -86,7 +86,8 @@ import tachiyomi.presentation.core.i18n.pluralStringResource
|
|||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.util.clickableNoIndication
|
import tachiyomi.presentation.core.util.clickableNoIndication
|
||||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||||
import kotlin.math.absoluteValue
|
import java.time.Instant
|
||||||
|
import java.time.temporal.ChronoUnit
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
|
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
|
||||||
@ -165,7 +166,7 @@ fun MangaInfoBox(
|
|||||||
fun MangaActionRow(
|
fun MangaActionRow(
|
||||||
favorite: Boolean,
|
favorite: Boolean,
|
||||||
trackingCount: Int,
|
trackingCount: Int,
|
||||||
fetchInterval: Int?,
|
nextUpdate: Instant?,
|
||||||
isUserIntervalMode: Boolean,
|
isUserIntervalMode: Boolean,
|
||||||
onAddToLibraryClicked: () -> Unit,
|
onAddToLibraryClicked: () -> Unit,
|
||||||
onWebViewClicked: (() -> Unit)?,
|
onWebViewClicked: (() -> Unit)?,
|
||||||
@ -177,6 +178,16 @@ fun MangaActionRow(
|
|||||||
) {
|
) {
|
||||||
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
|
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
|
||||||
|
|
||||||
|
// TODO: show something better when using custom interval
|
||||||
|
val nextUpdateDays = remember(nextUpdate) {
|
||||||
|
return@remember if (nextUpdate != null) {
|
||||||
|
val now = Instant.now()
|
||||||
|
now.until(nextUpdate, ChronoUnit.DAYS).toInt().coerceAtLeast(0)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
|
Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
|
||||||
MangaActionButton(
|
MangaActionButton(
|
||||||
title = if (favorite) {
|
title = if (favorite) {
|
||||||
@ -189,18 +200,20 @@ fun MangaActionRow(
|
|||||||
onClick = onAddToLibraryClicked,
|
onClick = onAddToLibraryClicked,
|
||||||
onLongClick = onEditCategory,
|
onLongClick = onEditCategory,
|
||||||
)
|
)
|
||||||
if (onEditIntervalClicked != null && fetchInterval != null) {
|
|
||||||
MangaActionButton(
|
MangaActionButton(
|
||||||
title = pluralStringResource(
|
title = if (nextUpdateDays != null) {
|
||||||
|
pluralStringResource(
|
||||||
MR.plurals.day,
|
MR.plurals.day,
|
||||||
count = fetchInterval.absoluteValue,
|
count = nextUpdateDays,
|
||||||
fetchInterval.absoluteValue,
|
nextUpdateDays,
|
||||||
),
|
)
|
||||||
|
} else {
|
||||||
|
stringResource(MR.strings.not_applicable)
|
||||||
|
},
|
||||||
icon = Icons.Default.HourglassEmpty,
|
icon = Icons.Default.HourglassEmpty,
|
||||||
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
||||||
onClick = onEditIntervalClicked,
|
onClick = { onEditIntervalClicked?.invoke() },
|
||||||
)
|
)
|
||||||
}
|
|
||||||
MangaActionButton(
|
MangaActionButton(
|
||||||
title = if (trackingCount == 0) {
|
title = if (trackingCount == 0) {
|
||||||
stringResource(MR.strings.manga_tracking_tab)
|
stringResource(MR.strings.manga_tracking_tab)
|
||||||
|
@ -17,7 +17,7 @@ import androidx.compose.ui.text.SpanStyle
|
|||||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||||
import com.halilibo.richtext.markdown.Markdown
|
import com.halilibo.richtext.markdown.Markdown
|
||||||
import com.halilibo.richtext.ui.RichTextStyle
|
import com.halilibo.richtext.ui.RichTextStyle
|
||||||
import com.halilibo.richtext.ui.material3.Material3RichText
|
import com.halilibo.richtext.ui.material3.RichText
|
||||||
import com.halilibo.richtext.ui.string.RichTextStringStyle
|
import com.halilibo.richtext.ui.string.RichTextStringStyle
|
||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
@ -42,7 +42,7 @@ fun NewUpdateScreen(
|
|||||||
rejectText = stringResource(MR.strings.action_not_now),
|
rejectText = stringResource(MR.strings.action_not_now),
|
||||||
onRejectClick = onRejectUpdate,
|
onRejectClick = onRejectUpdate,
|
||||||
) {
|
) {
|
||||||
Material3RichText(
|
RichText(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = MaterialTheme.padding.large),
|
.padding(vertical = MaterialTheme.padding.large),
|
||||||
|
@ -2,16 +2,22 @@ package eu.kanade.presentation.more.settings.screen
|
|||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.ReadOnlyComposable
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.presentation.more.settings.Preference
|
import eu.kanade.presentation.more.settings.Preference
|
||||||
|
import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen
|
||||||
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
|
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import tachiyomi.core.i18n.stringResource
|
import tachiyomi.core.i18n.stringResource
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
import tachiyomi.presentation.core.util.collectAsState
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
@ -24,7 +30,11 @@ object SettingsBrowseScreen : SearchableSettings {
|
|||||||
@Composable
|
@Composable
|
||||||
override fun getPreferences(): List<Preference> {
|
override fun getPreferences(): List<Preference> {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
|
||||||
val sourcePreferences = remember { Injekt.get<SourcePreferences>() }
|
val sourcePreferences = remember { Injekt.get<SourcePreferences>() }
|
||||||
|
val reposCount by sourcePreferences.extensionRepos().collectAsState()
|
||||||
|
|
||||||
return listOf(
|
return listOf(
|
||||||
Preference.PreferenceGroup(
|
Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.label_sources),
|
title = stringResource(MR.strings.label_sources),
|
||||||
@ -33,6 +43,13 @@ object SettingsBrowseScreen : SearchableSettings {
|
|||||||
pref = sourcePreferences.hideInLibraryItems(),
|
pref = sourcePreferences.hideInLibraryItems(),
|
||||||
title = stringResource(MR.strings.pref_hide_in_library_items),
|
title = stringResource(MR.strings.pref_hide_in_library_items),
|
||||||
),
|
),
|
||||||
|
Preference.PreferenceItem.TextPreference(
|
||||||
|
title = stringResource(MR.strings.label_extension_repos),
|
||||||
|
subtitle = pluralStringResource(MR.plurals.num_repos, reposCount.size, reposCount.size),
|
||||||
|
onClick = {
|
||||||
|
navigator.push(ExtensionReposScreen())
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Preference.PreferenceGroup(
|
Preference.PreferenceGroup(
|
||||||
|
@ -198,7 +198,7 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||||||
),
|
),
|
||||||
Preference.PreferenceItem.MultiSelectListPreference(
|
Preference.PreferenceItem.MultiSelectListPreference(
|
||||||
pref = libraryPreferences.autoUpdateMangaRestrictions(),
|
pref = libraryPreferences.autoUpdateMangaRestrictions(),
|
||||||
title = stringResource(MR.strings.pref_library_update_manga_restriction),
|
title = stringResource(MR.strings.pref_library_update_smart_update),
|
||||||
entries = persistentMapOf(
|
entries = persistentMapOf(
|
||||||
MANGA_HAS_UNREAD to stringResource(MR.strings.pref_update_only_completely_read),
|
MANGA_HAS_UNREAD to stringResource(MR.strings.pref_update_only_completely_read),
|
||||||
MANGA_NON_READ to stringResource(MR.strings.pref_update_only_started),
|
MANGA_NON_READ to stringResource(MR.strings.pref_update_only_started),
|
||||||
|
@ -0,0 +1,69 @@
|
|||||||
|
package eu.kanade.presentation.more.settings.screen.browse
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
|
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoCreateDialog
|
||||||
|
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoDeleteDialog
|
||||||
|
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionReposScreen
|
||||||
|
import eu.kanade.presentation.util.Screen
|
||||||
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
|
|
||||||
|
class ExtensionReposScreen : Screen() {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
val screenModel = rememberScreenModel { ExtensionReposScreenModel() }
|
||||||
|
|
||||||
|
val state by screenModel.state.collectAsState()
|
||||||
|
|
||||||
|
if (state is RepoScreenState.Loading) {
|
||||||
|
LoadingScreen()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val successState = state as RepoScreenState.Success
|
||||||
|
|
||||||
|
ExtensionReposScreen(
|
||||||
|
state = successState,
|
||||||
|
onClickCreate = { screenModel.showDialog(RepoDialog.Create) },
|
||||||
|
onClickDelete = { screenModel.showDialog(RepoDialog.Delete(it)) },
|
||||||
|
navigateUp = navigator::pop,
|
||||||
|
)
|
||||||
|
|
||||||
|
when (val dialog = successState.dialog) {
|
||||||
|
null -> {}
|
||||||
|
RepoDialog.Create -> {
|
||||||
|
ExtensionRepoCreateDialog(
|
||||||
|
onDismissRequest = screenModel::dismissDialog,
|
||||||
|
onCreate = { screenModel.createRepo(it) },
|
||||||
|
categories = successState.repos,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is RepoDialog.Delete -> {
|
||||||
|
ExtensionRepoDeleteDialog(
|
||||||
|
onDismissRequest = screenModel::dismissDialog,
|
||||||
|
onDelete = { screenModel.deleteRepo(dialog.repo) },
|
||||||
|
repo = dialog.repo,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
screenModel.events.collectLatest { event ->
|
||||||
|
if (event is RepoEvent.LocalizedMessage) {
|
||||||
|
context.toast(event.stringRes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,111 @@
|
|||||||
|
package eu.kanade.presentation.more.settings.screen.browse
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
|
import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
|
import dev.icerock.moko.resources.StringResource
|
||||||
|
import eu.kanade.domain.source.interactor.CreateSourceRepo
|
||||||
|
import eu.kanade.domain.source.interactor.DeleteSourceRepo
|
||||||
|
import eu.kanade.domain.source.interactor.GetSourceRepos
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import tachiyomi.core.util.lang.launchIO
|
||||||
|
import tachiyomi.i18n.MR
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class ExtensionReposScreenModel(
|
||||||
|
private val getSourceRepos: GetSourceRepos = Injekt.get(),
|
||||||
|
private val createSourceRepo: CreateSourceRepo = Injekt.get(),
|
||||||
|
private val deleteSourceRepo: DeleteSourceRepo = Injekt.get(),
|
||||||
|
) : StateScreenModel<RepoScreenState>(RepoScreenState.Loading) {
|
||||||
|
|
||||||
|
private val _events: Channel<RepoEvent> = Channel(Int.MAX_VALUE)
|
||||||
|
val events = _events.receiveAsFlow()
|
||||||
|
|
||||||
|
init {
|
||||||
|
screenModelScope.launchIO {
|
||||||
|
getSourceRepos.subscribe()
|
||||||
|
.collectLatest { repos ->
|
||||||
|
mutableState.update {
|
||||||
|
RepoScreenState.Success(
|
||||||
|
repos = repos.toImmutableList(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and adds a new repo to the database.
|
||||||
|
*
|
||||||
|
* @param name The name of the repo to create.
|
||||||
|
*/
|
||||||
|
fun createRepo(name: String) {
|
||||||
|
screenModelScope.launchIO {
|
||||||
|
when (createSourceRepo.await(name)) {
|
||||||
|
is CreateSourceRepo.Result.InvalidUrl -> _events.send(RepoEvent.InvalidUrl)
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the given repo from the database.
|
||||||
|
*
|
||||||
|
* @param repo The repo to delete.
|
||||||
|
*/
|
||||||
|
fun deleteRepo(repo: String) {
|
||||||
|
screenModelScope.launchIO {
|
||||||
|
deleteSourceRepo.await(repo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showDialog(dialog: RepoDialog) {
|
||||||
|
mutableState.update {
|
||||||
|
when (it) {
|
||||||
|
RepoScreenState.Loading -> it
|
||||||
|
is RepoScreenState.Success -> it.copy(dialog = dialog)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismissDialog() {
|
||||||
|
mutableState.update {
|
||||||
|
when (it) {
|
||||||
|
RepoScreenState.Loading -> it
|
||||||
|
is RepoScreenState.Success -> it.copy(dialog = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class RepoEvent {
|
||||||
|
sealed class LocalizedMessage(val stringRes: StringResource) : RepoEvent()
|
||||||
|
data object InvalidUrl : LocalizedMessage(MR.strings.invalid_repo_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class RepoDialog {
|
||||||
|
data object Create : RepoDialog()
|
||||||
|
data class Delete(val repo: String) : RepoDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class RepoScreenState {
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data object Loading : RepoScreenState()
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class Success(
|
||||||
|
val repos: ImmutableList<String>,
|
||||||
|
val dialog: RepoDialog? = null,
|
||||||
|
) : RepoScreenState() {
|
||||||
|
|
||||||
|
val isEmpty: Boolean
|
||||||
|
get() = repos.isEmpty()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,81 @@
|
|||||||
|
package eu.kanade.presentation.more.settings.screen.browse.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.outlined.Label
|
||||||
|
import androidx.compose.material.icons.outlined.Delete
|
||||||
|
import androidx.compose.material3.ElevatedCard
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ExtensionReposContent(
|
||||||
|
repos: ImmutableList<String>,
|
||||||
|
lazyListState: LazyListState,
|
||||||
|
paddingValues: PaddingValues,
|
||||||
|
onClickDelete: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
state = lazyListState,
|
||||||
|
contentPadding = paddingValues,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
items(repos) { repo ->
|
||||||
|
ExtensionRepoListItem(
|
||||||
|
modifier = Modifier.animateItemPlacement(),
|
||||||
|
repo = repo,
|
||||||
|
onDelete = { onClickDelete(repo) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ExtensionRepoListItem(
|
||||||
|
repo: String,
|
||||||
|
onDelete: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
ElevatedCard(
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(
|
||||||
|
start = MaterialTheme.padding.medium,
|
||||||
|
top = MaterialTheme.padding.medium,
|
||||||
|
end = MaterialTheme.padding.medium,
|
||||||
|
),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = null)
|
||||||
|
Text(text = repo, modifier = Modifier.padding(start = MaterialTheme.padding.medium))
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
) {
|
||||||
|
IconButton(onClick = onDelete) {
|
||||||
|
Icon(imageVector = Icons.Outlined.Delete, contentDescription = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,117 @@
|
|||||||
|
package eu.kanade.presentation.more.settings.screen.browse.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ExtensionRepoCreateDialog(
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onCreate: (String) -> Unit,
|
||||||
|
categories: ImmutableList<String>,
|
||||||
|
) {
|
||||||
|
var name by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
val nameAlreadyExists = remember(name) { categories.contains(name) }
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
enabled = name.isNotEmpty() && !nameAlreadyExists,
|
||||||
|
onClick = {
|
||||||
|
onCreate(name)
|
||||||
|
onDismissRequest()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(MR.strings.action_add))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismissRequest) {
|
||||||
|
Text(text = stringResource(MR.strings.action_cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(text = stringResource(MR.strings.action_add_repo))
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
Text(text = stringResource(MR.strings.action_add_repo_message))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier
|
||||||
|
.focusRequester(focusRequester),
|
||||||
|
value = name,
|
||||||
|
onValueChange = { name = it },
|
||||||
|
label = {
|
||||||
|
Text(text = stringResource(MR.strings.label_add_repo_input))
|
||||||
|
},
|
||||||
|
supportingText = {
|
||||||
|
val msgRes = if (name.isNotEmpty() && nameAlreadyExists) {
|
||||||
|
MR.strings.error_repo_exists
|
||||||
|
} else {
|
||||||
|
MR.strings.information_required_plain
|
||||||
|
}
|
||||||
|
Text(text = stringResource(msgRes))
|
||||||
|
},
|
||||||
|
isError = name.isNotEmpty() && nameAlreadyExists,
|
||||||
|
singleLine = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(focusRequester) {
|
||||||
|
// TODO: https://issuetracker.google.com/issues/204502668
|
||||||
|
delay(0.1.seconds)
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ExtensionRepoDeleteDialog(
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onDelete: () -> Unit,
|
||||||
|
repo: String,
|
||||||
|
) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
onDelete()
|
||||||
|
onDismissRequest()
|
||||||
|
}) {
|
||||||
|
Text(text = stringResource(MR.strings.action_ok))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismissRequest) {
|
||||||
|
Text(text = stringResource(MR.strings.action_cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(text = stringResource(MR.strings.action_delete_repo))
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(text = stringResource(MR.strings.delete_repo_confirmation, repo))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
@file:JvmName("ExtensionReposScreenKt")
|
||||||
|
|
||||||
|
package eu.kanade.presentation.more.settings.screen.browse.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import eu.kanade.presentation.category.components.CategoryFloatingActionButton
|
||||||
|
import eu.kanade.presentation.components.AppBar
|
||||||
|
import eu.kanade.presentation.more.settings.screen.browse.RepoScreenState
|
||||||
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
import tachiyomi.presentation.core.components.material.topSmallPaddingValues
|
||||||
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||||
|
import tachiyomi.presentation.core.util.plus
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ExtensionReposScreen(
|
||||||
|
state: RepoScreenState.Success,
|
||||||
|
onClickCreate: () -> Unit,
|
||||||
|
onClickDelete: (String) -> Unit,
|
||||||
|
navigateUp: () -> Unit,
|
||||||
|
) {
|
||||||
|
val lazyListState = rememberLazyListState()
|
||||||
|
Scaffold(
|
||||||
|
topBar = { scrollBehavior ->
|
||||||
|
AppBar(
|
||||||
|
navigateUp = navigateUp,
|
||||||
|
title = stringResource(MR.strings.label_extension_repos),
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
floatingActionButton = {
|
||||||
|
CategoryFloatingActionButton(
|
||||||
|
lazyListState = lazyListState,
|
||||||
|
onCreate = onClickCreate,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { paddingValues ->
|
||||||
|
if (state.isEmpty) {
|
||||||
|
EmptyScreen(
|
||||||
|
MR.strings.information_empty_repos,
|
||||||
|
modifier = Modifier.padding(paddingValues),
|
||||||
|
)
|
||||||
|
return@Scaffold
|
||||||
|
}
|
||||||
|
|
||||||
|
ExtensionReposContent(
|
||||||
|
repos = state.repos,
|
||||||
|
lazyListState = lazyListState,
|
||||||
|
paddingValues = paddingValues + topSmallPaddingValues +
|
||||||
|
PaddingValues(horizontal = MaterialTheme.padding.medium),
|
||||||
|
onClickDelete = onClickDelete,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -1,54 +1,13 @@
|
|||||||
package eu.kanade.presentation.util
|
package eu.kanade.presentation.util
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import androidx.activity.BackEventCompat
|
|
||||||
import androidx.activity.compose.PredictiveBackHandler
|
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||||
import androidx.compose.animation.ContentTransform
|
import androidx.compose.animation.ContentTransform
|
||||||
import androidx.compose.animation.EnterTransition
|
|
||||||
import androidx.compose.animation.ExitTransition
|
|
||||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
|
||||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
|
||||||
import androidx.compose.animation.core.animate
|
|
||||||
import androidx.compose.animation.core.tween
|
|
||||||
import androidx.compose.animation.togetherWith
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.ProvidableCompositionLocal
|
import androidx.compose.runtime.ProvidableCompositionLocal
|
||||||
import androidx.compose.runtime.Stable
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.movableContentOf
|
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.runtime.staticCompositionLocalOf
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.drawWithCache
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.geometry.Rect
|
|
||||||
import androidx.compose.ui.geometry.Size
|
|
||||||
import androidx.compose.ui.graphics.BlurEffect
|
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
|
||||||
import androidx.compose.ui.graphics.ColorMatrix
|
|
||||||
import androidx.compose.ui.graphics.Paint
|
|
||||||
import androidx.compose.ui.graphics.RectangleShape
|
|
||||||
import androidx.compose.ui.graphics.TransformOrigin
|
|
||||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.layout.onSizeChanged
|
|
||||||
import androidx.compose.ui.platform.LocalView
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.toSize
|
|
||||||
import androidx.compose.ui.util.lerp
|
|
||||||
import androidx.compose.ui.zIndex
|
|
||||||
import cafe.adriel.voyager.core.model.ScreenModel
|
import cafe.adriel.voyager.core.model.ScreenModel
|
||||||
import cafe.adriel.voyager.core.model.ScreenModelStore
|
import cafe.adriel.voyager.core.model.ScreenModelStore
|
||||||
import cafe.adriel.voyager.core.screen.Screen
|
import cafe.adriel.voyager.core.screen.Screen
|
||||||
@ -57,25 +16,14 @@ import cafe.adriel.voyager.core.screen.uniqueScreenKey
|
|||||||
import cafe.adriel.voyager.core.stack.StackEvent
|
import cafe.adriel.voyager.core.stack.StackEvent
|
||||||
import cafe.adriel.voyager.navigator.Navigator
|
import cafe.adriel.voyager.navigator.Navigator
|
||||||
import cafe.adriel.voyager.transitions.ScreenTransitionContent
|
import cafe.adriel.voyager.transitions.ScreenTransitionContent
|
||||||
import eu.kanade.tachiyomi.util.view.getWindowRadius
|
|
||||||
import kotlinx.coroutines.CoroutineName
|
import kotlinx.coroutines.CoroutineName
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.awaitAll
|
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.flow.onCompletion
|
|
||||||
import kotlinx.coroutines.flow.onStart
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import soup.compose.material.motion.MotionConstants
|
|
||||||
import soup.compose.material.motion.animation.materialSharedAxisX
|
import soup.compose.material.motion.animation.materialSharedAxisX
|
||||||
import soup.compose.material.motion.animation.rememberSlideDistance
|
import soup.compose.material.motion.animation.rememberSlideDistance
|
||||||
import kotlin.coroutines.cancellation.CancellationException
|
|
||||||
import kotlin.math.PI
|
|
||||||
import kotlin.math.sin
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For invoking back press to the parent activity
|
* For invoking back press to the parent activity
|
||||||
@ -109,299 +57,17 @@ interface AssistContentScreen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DefaultNavigatorScreenTransition(
|
fun DefaultNavigatorScreenTransition(navigator: Navigator) {
|
||||||
navigator: Navigator,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val view = LocalView.current
|
|
||||||
val handler = remember {
|
|
||||||
OnBackHandler(
|
|
||||||
scope = scope,
|
|
||||||
windowCornerRadius = view.getWindowRadius(),
|
|
||||||
onBackPressed = navigator::pop,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
PredictiveBackHandler(enabled = navigator.canPop) { progress ->
|
|
||||||
progress
|
|
||||||
.onStart { handler.reset() }
|
|
||||||
.onCompletion { e ->
|
|
||||||
if (e == null) {
|
|
||||||
handler.onBackConfirmed()
|
|
||||||
} else {
|
|
||||||
handler.onBackCancelled()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.collect(handler::onBackEvent)
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(modifier = modifier.onSizeChanged { handler.updateContainerSize(it.toSize()) }) {
|
|
||||||
val currentSceneEntry = navigator.lastItem
|
|
||||||
val showPrev by remember {
|
|
||||||
derivedStateOf { handler.scale < 1f || handler.translationY != 0f }
|
|
||||||
}
|
|
||||||
val visibleItems = remember(currentSceneEntry, showPrev) {
|
|
||||||
if (showPrev) {
|
|
||||||
val prevSceneEntry = navigator.items.getOrNull(navigator.size - 2)
|
|
||||||
listOfNotNull(currentSceneEntry, prevSceneEntry)
|
|
||||||
} else {
|
|
||||||
listOfNotNull(currentSceneEntry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val slideDistance = rememberSlideDistance()
|
val slideDistance = rememberSlideDistance()
|
||||||
|
ScreenTransition(
|
||||||
val screenContent = remember {
|
navigator = navigator,
|
||||||
movableContentOf<Screen> { screen ->
|
transition = {
|
||||||
navigator.saveableState("transition", screen) {
|
|
||||||
screen.Content()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
visibleItems.forEachIndexed { index, backStackEntry ->
|
|
||||||
val isPrev = index == 1 && visibleItems.size > 1
|
|
||||||
if (!isPrev) {
|
|
||||||
AnimatedContent(
|
|
||||||
targetState = backStackEntry,
|
|
||||||
transitionSpec = {
|
|
||||||
val forward = navigator.lastEvent != StackEvent.Pop
|
|
||||||
if (!forward && !handler.isReady) {
|
|
||||||
// Pop screen without animation when predictive back is in use
|
|
||||||
EnterTransition.None togetherWith ExitTransition.None
|
|
||||||
} else {
|
|
||||||
materialSharedAxisX(
|
materialSharedAxisX(
|
||||||
forward = forward,
|
forward = navigator.lastEvent != StackEvent.Pop,
|
||||||
slideDistance = slideDistance,
|
slideDistance = slideDistance,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.zIndex(1f)
|
|
||||||
.graphicsLayer {
|
|
||||||
this.alpha = handler.alpha
|
|
||||||
this.transformOrigin = TransformOrigin(
|
|
||||||
pivotFractionX = if (handler.swipeEdge == BackEventCompat.EDGE_LEFT) 0.8f else 0.2f,
|
|
||||||
pivotFractionY = 0.5f,
|
|
||||||
)
|
|
||||||
this.scaleX = handler.scale
|
|
||||||
this.scaleY = handler.scale
|
|
||||||
this.translationY = handler.translationY
|
|
||||||
this.clip = true
|
|
||||||
this.shape = if (showPrev) {
|
|
||||||
RoundedCornerShape(handler.windowCornerRadius.toFloat())
|
|
||||||
} else {
|
|
||||||
RectangleShape
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.then(
|
|
||||||
if (showPrev) {
|
|
||||||
Modifier.pointerInput(Unit) {
|
|
||||||
// Animated content should not be interactive
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Modifier
|
|
||||||
},
|
|
||||||
),
|
|
||||||
content = {
|
|
||||||
if (visibleItems.size == 2 && visibleItems.getOrNull(1) == it) {
|
|
||||||
// Avoid drawing previous screen
|
|
||||||
return@AnimatedContent
|
|
||||||
}
|
|
||||||
screenContent(it)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.zIndex(0f)
|
|
||||||
.drawWithCache {
|
|
||||||
val bounds = Rect(Offset.Zero, size)
|
|
||||||
val matrix = ColorMatrix().apply {
|
|
||||||
// Reduce saturation and brightness
|
|
||||||
setToSaturation(lerp(1f, 0.95f, handler.alpha))
|
|
||||||
set(0, 4, lerp(0f, -25f, handler.alpha))
|
|
||||||
set(1, 4, lerp(0f, -25f, handler.alpha))
|
|
||||||
set(2, 4, lerp(0f, -25f, handler.alpha))
|
|
||||||
}
|
|
||||||
val paint = Paint().apply { colorFilter = ColorFilter.colorMatrix(matrix) }
|
|
||||||
onDrawWithContent {
|
|
||||||
drawIntoCanvas {
|
|
||||||
it.saveLayer(bounds, paint)
|
|
||||||
drawContent()
|
|
||||||
it.restore()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.graphicsLayer {
|
|
||||||
val blurRadius = 5.dp.toPx() * handler.alpha
|
|
||||||
renderEffect = if (blurRadius > 0f) {
|
|
||||||
BlurEffect(blurRadius, blurRadius)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.pointerInput(Unit) {
|
|
||||||
// bg content should not be interactive
|
|
||||||
},
|
|
||||||
content = { screenContent(backStackEntry) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(currentSceneEntry) {
|
|
||||||
// Reset *after* the screen is popped successfully
|
|
||||||
// so that the correct transition is applied
|
|
||||||
handler.setReady()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Stable
|
|
||||||
private class OnBackHandler(
|
|
||||||
private val scope: CoroutineScope,
|
|
||||||
val windowCornerRadius: Int,
|
|
||||||
private val onBackPressed: () -> Unit,
|
|
||||||
) {
|
|
||||||
|
|
||||||
var isReady = true
|
|
||||||
private set
|
|
||||||
|
|
||||||
var alpha by mutableFloatStateOf(1f)
|
|
||||||
private set
|
|
||||||
|
|
||||||
var scale by mutableFloatStateOf(1f)
|
|
||||||
private set
|
|
||||||
|
|
||||||
var translationY by mutableFloatStateOf(0f)
|
|
||||||
private set
|
|
||||||
|
|
||||||
var swipeEdge by mutableIntStateOf(BackEventCompat.EDGE_LEFT)
|
|
||||||
private set
|
|
||||||
|
|
||||||
private var containerSize = Size.Zero
|
|
||||||
private var startPointY = Float.NaN
|
|
||||||
|
|
||||||
var isPredictiveBack by mutableStateOf(false)
|
|
||||||
private set
|
|
||||||
|
|
||||||
private var animationJob: Job? = null
|
|
||||||
set(value) {
|
|
||||||
isReady = false
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateContainerSize(size: Size) {
|
|
||||||
containerSize = size
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setReady() {
|
|
||||||
reset()
|
|
||||||
animationJob?.cancel()
|
|
||||||
animationJob = null
|
|
||||||
isReady = true
|
|
||||||
isPredictiveBack = false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reset() {
|
|
||||||
startPointY = Float.NaN
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onBackEvent(backEvent: BackEventCompat) {
|
|
||||||
if (!isReady) return
|
|
||||||
isPredictiveBack = true
|
|
||||||
swipeEdge = backEvent.swipeEdge
|
|
||||||
|
|
||||||
val progress = LinearOutSlowInEasing.transform(backEvent.progress)
|
|
||||||
scale = lerp(1f, 0.85f, progress)
|
|
||||||
|
|
||||||
if (startPointY.isNaN()) {
|
|
||||||
startPointY = backEvent.touchY
|
|
||||||
}
|
|
||||||
val deltaYRatio = (backEvent.touchY - startPointY) / containerSize.height
|
|
||||||
val translateYDistance = containerSize.height / 20
|
|
||||||
translationY = sin(deltaYRatio * PI * 0.5).toFloat() * translateYDistance * progress
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onBackConfirmed() {
|
|
||||||
if (!isReady) return
|
|
||||||
if (isPredictiveBack) {
|
|
||||||
// Continue predictive animation and pop the screen
|
|
||||||
val animationSpec = tween<Float>(
|
|
||||||
durationMillis = MotionConstants.DefaultMotionDuration,
|
|
||||||
easing = FastOutSlowInEasing,
|
|
||||||
)
|
|
||||||
animationJob = scope.launch {
|
|
||||||
try {
|
|
||||||
listOf(
|
|
||||||
async {
|
|
||||||
animate(
|
|
||||||
initialValue = alpha,
|
|
||||||
targetValue = 0f,
|
|
||||||
animationSpec = animationSpec,
|
|
||||||
) { value, _ ->
|
|
||||||
alpha = value
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async {
|
|
||||||
animate(
|
|
||||||
initialValue = scale,
|
|
||||||
targetValue = scale - 0.05f,
|
|
||||||
animationSpec = animationSpec,
|
|
||||||
) { value, _ ->
|
|
||||||
scale = value
|
|
||||||
}
|
|
||||||
},
|
|
||||||
).awaitAll()
|
|
||||||
} catch (e: CancellationException) {
|
|
||||||
// no-op
|
|
||||||
} finally {
|
|
||||||
onBackPressed()
|
|
||||||
alpha = 1f
|
|
||||||
translationY = 0f
|
|
||||||
scale = 1f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Pop right away and use default transition
|
|
||||||
onBackPressed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onBackCancelled() {
|
|
||||||
// Reset states
|
|
||||||
isPredictiveBack = false
|
|
||||||
animationJob = scope.launch {
|
|
||||||
listOf(
|
|
||||||
async {
|
|
||||||
animate(
|
|
||||||
initialValue = scale,
|
|
||||||
targetValue = 1f,
|
|
||||||
) { value, _ ->
|
|
||||||
scale = value
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async {
|
|
||||||
animate(
|
|
||||||
initialValue = alpha,
|
|
||||||
targetValue = 1f,
|
|
||||||
) { value, _ ->
|
|
||||||
alpha = value
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async {
|
|
||||||
animate(
|
|
||||||
initialValue = translationY,
|
|
||||||
targetValue = 0f,
|
|
||||||
) { value, _ ->
|
|
||||||
translationY = value
|
|
||||||
}
|
|
||||||
},
|
|
||||||
).awaitAll()
|
|
||||||
|
|
||||||
isReady = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -405,6 +405,11 @@ object Migrations {
|
|||||||
// Deleting old download cache index files, but might as well clear it all out
|
// Deleting old download cache index files, but might as well clear it all out
|
||||||
context.cacheDir.deleteRecursively()
|
context.cacheDir.deleteRecursively()
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 114) {
|
||||||
|
sourcePreferences.extensionRepos().getAndSet {
|
||||||
|
it.map { "https://raw.githubusercontent.com/$it/repo" }.toSet()
|
||||||
|
}
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,6 +28,7 @@ import nl.adaptivity.xmlutil.XmlDeclMode
|
|||||||
import nl.adaptivity.xmlutil.core.XmlVersion
|
import nl.adaptivity.xmlutil.core.XmlVersion
|
||||||
import nl.adaptivity.xmlutil.serialization.XML
|
import nl.adaptivity.xmlutil.serialization.XML
|
||||||
import tachiyomi.core.storage.AndroidStorageFolderProvider
|
import tachiyomi.core.storage.AndroidStorageFolderProvider
|
||||||
|
import tachiyomi.core.storage.UniFileTempFileManager
|
||||||
import tachiyomi.data.AndroidDatabaseHandler
|
import tachiyomi.data.AndroidDatabaseHandler
|
||||||
import tachiyomi.data.Database
|
import tachiyomi.data.Database
|
||||||
import tachiyomi.data.DatabaseHandler
|
import tachiyomi.data.DatabaseHandler
|
||||||
@ -112,6 +113,8 @@ class AppModule(val app: Application) : InjektModule {
|
|||||||
ProtoBuf
|
ProtoBuf
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addSingletonFactory { UniFileTempFileManager(app) }
|
||||||
|
|
||||||
addSingletonFactory { ChapterCache(app, get()) }
|
addSingletonFactory { ChapterCache(app, get()) }
|
||||||
addSingletonFactory { CoverCache(app) }
|
addSingletonFactory { CoverCache(app) }
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.extension
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
import eu.kanade.tachiyomi.extension.api.ExtensionApi
|
||||||
import eu.kanade.tachiyomi.extension.api.ExtensionUpdateNotifier
|
import eu.kanade.tachiyomi.extension.api.ExtensionUpdateNotifier
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||||
@ -49,7 +49,7 @@ class ExtensionManager(
|
|||||||
/**
|
/**
|
||||||
* API where all the available extensions can be found.
|
* API where all the available extensions can be found.
|
||||||
*/
|
*/
|
||||||
private val api = ExtensionGithubApi()
|
private val api = ExtensionApi()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The installer which installs, updates and uninstalls the extensions.
|
* The installer which installs, updates and uninstalls the extensions.
|
||||||
@ -258,7 +258,6 @@ class ExtensionManager(
|
|||||||
val untrustedSignatures = _untrustedExtensionsFlow.value.map { it.signatureHash }.toSet()
|
val untrustedSignatures = _untrustedExtensionsFlow.value.map { it.signatureHash }.toSet()
|
||||||
if (signature !in untrustedSignatures) return
|
if (signature !in untrustedSignatures) return
|
||||||
|
|
||||||
ExtensionLoader.trustedSignatures += signature
|
|
||||||
preferences.trustedSignatures() += signature
|
preferences.trustedSignatures() += signature
|
||||||
|
|
||||||
val nowTrustedExtensions = _untrustedExtensionsFlow.value.filter { it.signatureHash == signature }
|
val nowTrustedExtensions = _untrustedExtensionsFlow.value.filter { it.signatureHash == signature }
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi.extension.api
|
package eu.kanade.tachiyomi.extension.api
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import eu.kanade.domain.source.interactor.OFFICIAL_REPO_BASE_URL
|
||||||
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
import eu.kanade.tachiyomi.extension.model.LoadResult
|
||||||
@ -20,10 +22,11 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import kotlin.time.Duration.Companion.days
|
import kotlin.time.Duration.Companion.days
|
||||||
|
|
||||||
internal class ExtensionGithubApi {
|
internal class ExtensionApi {
|
||||||
|
|
||||||
private val networkService: NetworkHelper by injectLazy()
|
private val networkService: NetworkHelper by injectLazy()
|
||||||
private val preferenceStore: PreferenceStore by injectLazy()
|
private val preferenceStore: PreferenceStore by injectLazy()
|
||||||
|
private val sourcePreferences: SourcePreferences by injectLazy()
|
||||||
private val extensionManager: ExtensionManager by injectLazy()
|
private val extensionManager: ExtensionManager by injectLazy()
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
@ -31,39 +34,16 @@ internal class ExtensionGithubApi {
|
|||||||
preferenceStore.getLong(Preference.appStateKey("last_ext_check"), 0)
|
preferenceStore.getLong(Preference.appStateKey("last_ext_check"), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var requiresFallbackSource = false
|
|
||||||
|
|
||||||
suspend fun findExtensions(): List<Extension.Available> {
|
suspend fun findExtensions(): List<Extension.Available> {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val githubResponse = if (requiresFallbackSource) {
|
val extensions = buildList {
|
||||||
null
|
addAll(getExtensions(OFFICIAL_REPO_BASE_URL, true))
|
||||||
} else {
|
sourcePreferences.extensionRepos().get().map { addAll(getExtensions(it, false)) }
|
||||||
try {
|
|
||||||
networkService.client
|
|
||||||
.newCall(GET("${REPO_URL_PREFIX}index.min.json"))
|
|
||||||
.awaitSuccess()
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
logcat(LogPriority.ERROR, e) { "Failed to get extensions from GitHub" }
|
|
||||||
requiresFallbackSource = true
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val response = githubResponse ?: run {
|
|
||||||
networkService.client
|
|
||||||
.newCall(GET("${FALLBACK_REPO_URL_PREFIX}index.min.json"))
|
|
||||||
.awaitSuccess()
|
|
||||||
}
|
|
||||||
|
|
||||||
val extensions = with(json) {
|
|
||||||
response
|
|
||||||
.parseAs<List<ExtensionJsonObject>>()
|
|
||||||
.toExtensions()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanity check - a small number of extensions probably means something broke
|
// Sanity check - a small number of extensions probably means something broke
|
||||||
// with the repo generator
|
// with the repo generator
|
||||||
if (extensions.size < 100) {
|
if (extensions.size < 50) {
|
||||||
throw Exception()
|
throw Exception()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,6 +51,26 @@ internal class ExtensionGithubApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun getExtensions(
|
||||||
|
repoBaseUrl: String,
|
||||||
|
isOfficialRepo: Boolean,
|
||||||
|
): List<Extension.Available> {
|
||||||
|
return try {
|
||||||
|
val response = networkService.client
|
||||||
|
.newCall(GET("$repoBaseUrl/index.min.json"))
|
||||||
|
.awaitSuccess()
|
||||||
|
|
||||||
|
with(json) {
|
||||||
|
response
|
||||||
|
.parseAs<List<ExtensionJsonObject>>()
|
||||||
|
.toExtensions(repoBaseUrl, isRepoSource = !isOfficialRepo)
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
logcat(LogPriority.ERROR, e) { "Failed to get extensions from $repoBaseUrl" }
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun checkForUpdates(
|
suspend fun checkForUpdates(
|
||||||
context: Context,
|
context: Context,
|
||||||
fromAvailableExtensionList: Boolean = false,
|
fromAvailableExtensionList: Boolean = false,
|
||||||
@ -111,7 +111,10 @@ internal class ExtensionGithubApi {
|
|||||||
return extensionsWithUpdate
|
return extensionsWithUpdate
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun List<ExtensionJsonObject>.toExtensions(): List<Extension.Available> {
|
private fun List<ExtensionJsonObject>.toExtensions(
|
||||||
|
repoUrl: String,
|
||||||
|
isRepoSource: Boolean,
|
||||||
|
): List<Extension.Available> {
|
||||||
return this
|
return this
|
||||||
.filter {
|
.filter {
|
||||||
val libVersion = it.extractLibVersion()
|
val libVersion = it.extractLibVersion()
|
||||||
@ -126,25 +129,17 @@ internal class ExtensionGithubApi {
|
|||||||
libVersion = it.extractLibVersion(),
|
libVersion = it.extractLibVersion(),
|
||||||
lang = it.lang,
|
lang = it.lang,
|
||||||
isNsfw = it.nsfw == 1,
|
isNsfw = it.nsfw == 1,
|
||||||
hasReadme = it.hasReadme == 1,
|
|
||||||
hasChangelog = it.hasChangelog == 1,
|
|
||||||
sources = it.sources?.map(extensionSourceMapper).orEmpty(),
|
sources = it.sources?.map(extensionSourceMapper).orEmpty(),
|
||||||
apkName = it.apk,
|
apkName = it.apk,
|
||||||
iconUrl = "${getUrlPrefix()}icon/${it.pkg}.png",
|
iconUrl = "$repoUrl/icon/${it.pkg}.png",
|
||||||
|
repoUrl = repoUrl,
|
||||||
|
isRepoSource = isRepoSource,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getApkUrl(extension: Extension.Available): String {
|
fun getApkUrl(extension: Extension.Available): String {
|
||||||
return "${getUrlPrefix()}apk/${extension.apkName}"
|
return "${extension.repoUrl}/apk/${extension.apkName}"
|
||||||
}
|
|
||||||
|
|
||||||
private fun getUrlPrefix(): String {
|
|
||||||
return if (requiresFallbackSource) {
|
|
||||||
FALLBACK_REPO_URL_PREFIX
|
|
||||||
} else {
|
|
||||||
REPO_URL_PREFIX
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ExtensionJsonObject.extractLibVersion(): Double {
|
private fun ExtensionJsonObject.extractLibVersion(): Double {
|
||||||
@ -152,9 +147,6 @@ internal class ExtensionGithubApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val REPO_URL_PREFIX = "https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/"
|
|
||||||
private const val FALLBACK_REPO_URL_PREFIX = "https://gcore.jsdelivr.net/gh/tachiyomiorg/tachiyomi-extensions@repo/"
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
private data class ExtensionJsonObject(
|
private data class ExtensionJsonObject(
|
||||||
val name: String,
|
val name: String,
|
||||||
@ -164,8 +156,6 @@ private data class ExtensionJsonObject(
|
|||||||
val code: Long,
|
val code: Long,
|
||||||
val version: String,
|
val version: String,
|
||||||
val nsfw: Int,
|
val nsfw: Int,
|
||||||
val hasReadme: Int = 0,
|
|
||||||
val hasChangelog: Int = 0,
|
|
||||||
val sources: List<ExtensionSourceJsonObject>?,
|
val sources: List<ExtensionSourceJsonObject>?,
|
||||||
)
|
)
|
||||||
|
|
@ -13,8 +13,6 @@ sealed class Extension {
|
|||||||
abstract val libVersion: Double
|
abstract val libVersion: Double
|
||||||
abstract val lang: String?
|
abstract val lang: String?
|
||||||
abstract val isNsfw: Boolean
|
abstract val isNsfw: Boolean
|
||||||
abstract val hasReadme: Boolean
|
|
||||||
abstract val hasChangelog: Boolean
|
|
||||||
|
|
||||||
data class Installed(
|
data class Installed(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
@ -24,8 +22,6 @@ sealed class Extension {
|
|||||||
override val libVersion: Double,
|
override val libVersion: Double,
|
||||||
override val lang: String,
|
override val lang: String,
|
||||||
override val isNsfw: Boolean,
|
override val isNsfw: Boolean,
|
||||||
override val hasReadme: Boolean,
|
|
||||||
override val hasChangelog: Boolean,
|
|
||||||
val pkgFactory: String?,
|
val pkgFactory: String?,
|
||||||
val sources: List<Source>,
|
val sources: List<Source>,
|
||||||
val icon: Drawable?,
|
val icon: Drawable?,
|
||||||
@ -33,6 +29,8 @@ sealed class Extension {
|
|||||||
val isObsolete: Boolean = false,
|
val isObsolete: Boolean = false,
|
||||||
val isUnofficial: Boolean = false,
|
val isUnofficial: Boolean = false,
|
||||||
val isShared: Boolean,
|
val isShared: Boolean,
|
||||||
|
val repoUrl: String? = null,
|
||||||
|
val isRepoSource: Boolean = false,
|
||||||
) : Extension()
|
) : Extension()
|
||||||
|
|
||||||
data class Available(
|
data class Available(
|
||||||
@ -43,11 +41,11 @@ sealed class Extension {
|
|||||||
override val libVersion: Double,
|
override val libVersion: Double,
|
||||||
override val lang: String,
|
override val lang: String,
|
||||||
override val isNsfw: Boolean,
|
override val isNsfw: Boolean,
|
||||||
override val hasReadme: Boolean,
|
|
||||||
override val hasChangelog: Boolean,
|
|
||||||
val sources: List<Source>,
|
val sources: List<Source>,
|
||||||
val apkName: String,
|
val apkName: String,
|
||||||
val iconUrl: String,
|
val iconUrl: String,
|
||||||
|
val repoUrl: String,
|
||||||
|
val isRepoSource: Boolean,
|
||||||
) : Extension() {
|
) : Extension() {
|
||||||
|
|
||||||
data class Source(
|
data class Source(
|
||||||
@ -75,7 +73,5 @@ sealed class Extension {
|
|||||||
val signatureHash: String,
|
val signatureHash: String,
|
||||||
override val lang: String? = null,
|
override val lang: String? = null,
|
||||||
override val isNsfw: Boolean = false,
|
override val isNsfw: Boolean = false,
|
||||||
override val hasReadme: Boolean = false,
|
|
||||||
override val hasChangelog: Boolean = false,
|
|
||||||
) : Extension()
|
) : Extension()
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ import eu.kanade.tachiyomi.source.Source
|
|||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
import eu.kanade.tachiyomi.util.lang.Hash
|
import eu.kanade.tachiyomi.util.lang.Hash
|
||||||
import eu.kanade.tachiyomi.util.storage.copyAndSetReadOnlyTo
|
import eu.kanade.tachiyomi.util.storage.copyAndSetReadOnlyTo
|
||||||
|
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
@ -62,11 +63,6 @@ internal object ExtensionLoader {
|
|||||||
// inorichi's key
|
// inorichi's key
|
||||||
private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
|
private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
|
||||||
|
|
||||||
/**
|
|
||||||
* List of the trusted signatures.
|
|
||||||
*/
|
|
||||||
var trustedSignatures = mutableSetOf(officialSignature) + preferences.trustedSignatures().get()
|
|
||||||
|
|
||||||
private const val PRIVATE_EXTENSION_EXTENSION = "ext"
|
private const val PRIVATE_EXTENSION_EXTENSION = "ext"
|
||||||
|
|
||||||
private fun getPrivateExtensionDir(context: Context) = File(context.filesDir, "exts")
|
private fun getPrivateExtensionDir(context: Context) = File(context.filesDir, "exts")
|
||||||
@ -123,6 +119,12 @@ internal object ExtensionLoader {
|
|||||||
* @param context The application context.
|
* @param context The application context.
|
||||||
*/
|
*/
|
||||||
fun loadExtensions(context: Context): List<LoadResult> {
|
fun loadExtensions(context: Context): List<LoadResult> {
|
||||||
|
// Always make users trust unknown extensions on cold starts in non-dev builds
|
||||||
|
// due to inherent security risks
|
||||||
|
if (!isDevFlavor) {
|
||||||
|
preferences.trustedSignatures().delete()
|
||||||
|
}
|
||||||
|
|
||||||
val pkgManager = context.packageManager
|
val pkgManager = context.packageManager
|
||||||
|
|
||||||
val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
@ -329,8 +331,6 @@ internal object ExtensionLoader {
|
|||||||
libVersion = libVersion,
|
libVersion = libVersion,
|
||||||
lang = lang,
|
lang = lang,
|
||||||
isNsfw = isNsfw,
|
isNsfw = isNsfw,
|
||||||
hasReadme = hasReadme,
|
|
||||||
hasChangelog = hasChangelog,
|
|
||||||
sources = sources,
|
sources = sources,
|
||||||
pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY),
|
pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY),
|
||||||
isUnofficial = !isOfficiallySigned(signatures),
|
isUnofficial = !isOfficiallySigned(signatures),
|
||||||
@ -394,6 +394,11 @@ internal object ExtensionLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun hasTrustedSignature(signatures: List<String>): Boolean {
|
private fun hasTrustedSignature(signatures: List<String>): Boolean {
|
||||||
|
if (officialSignature in signatures) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
val trustedSignatures = preferences.trustedSignatures().get()
|
||||||
return trustedSignatures.any { signatures.contains(it) }
|
return trustedSignatures.any { signatures.contains(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +37,6 @@ data class ExtensionDetailsScreen(
|
|||||||
state = state,
|
state = state,
|
||||||
onClickSourcePreferences = { navigator.push(SourcePreferencesScreen(it)) },
|
onClickSourcePreferences = { navigator.push(SourcePreferencesScreen(it)) },
|
||||||
onClickWhatsNew = { uriHandler.openUri(screenModel.getChangelogUrl()) },
|
onClickWhatsNew = { uriHandler.openUri(screenModel.getChangelogUrl()) },
|
||||||
onClickReadme = { uriHandler.openUri(screenModel.getReadmeUrl()) },
|
|
||||||
onClickEnableAll = { screenModel.toggleSources(true) },
|
onClickEnableAll = { screenModel.toggleSources(true) },
|
||||||
onClickDisableAll = { screenModel.toggleSources(false) },
|
onClickDisableAll = { screenModel.toggleSources(false) },
|
||||||
onClickClearCookies = screenModel::clearCookies,
|
onClickClearCookies = screenModel::clearCookies,
|
||||||
|
@ -31,8 +31,6 @@ import uy.kohesive.injekt.api.get
|
|||||||
|
|
||||||
private const val URL_EXTENSION_COMMITS =
|
private const val URL_EXTENSION_COMMITS =
|
||||||
"https://github.com/tachiyomiorg/tachiyomi-extensions/commits/master"
|
"https://github.com/tachiyomiorg/tachiyomi-extensions/commits/master"
|
||||||
private const val URL_EXTENSION_BLOB =
|
|
||||||
"https://github.com/tachiyomiorg/tachiyomi-extensions/blob/master"
|
|
||||||
|
|
||||||
class ExtensionDetailsScreenModel(
|
class ExtensionDetailsScreenModel(
|
||||||
pkgName: String,
|
pkgName: String,
|
||||||
@ -93,26 +91,11 @@ class ExtensionDetailsScreenModel(
|
|||||||
|
|
||||||
val pkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.extension.")
|
val pkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.extension.")
|
||||||
val pkgFactory = extension.pkgFactory
|
val pkgFactory = extension.pkgFactory
|
||||||
if (extension.hasChangelog) {
|
|
||||||
return createUrl(URL_EXTENSION_BLOB, pkgName, pkgFactory, "/CHANGELOG.md")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Falling back on GitHub commit history because there is no explicit changelog in extension
|
// Falling back on GitHub commit history because there is no explicit changelog in extension
|
||||||
return createUrl(URL_EXTENSION_COMMITS, pkgName, pkgFactory)
|
return createUrl(URL_EXTENSION_COMMITS, pkgName, pkgFactory)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getReadmeUrl(): String {
|
|
||||||
val extension = state.value.extension ?: return ""
|
|
||||||
|
|
||||||
if (!extension.hasReadme) {
|
|
||||||
return "https://tachiyomi.org/docs/faq/browse/extensions"
|
|
||||||
}
|
|
||||||
|
|
||||||
val pkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.extension.")
|
|
||||||
val pkgFactory = extension.pkgFactory
|
|
||||||
return createUrl(URL_EXTENSION_BLOB, pkgName, pkgFactory, "/README.md")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clearCookies() {
|
fun clearCookies() {
|
||||||
val extension = state.value.extension ?: return
|
val extension = state.value.extension ?: return
|
||||||
|
|
||||||
|
@ -24,6 +24,8 @@ import eu.kanade.tachiyomi.data.cache.CoverCache
|
|||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.util.removeCovers
|
import eu.kanade.tachiyomi.util.removeCovers
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.emptyFlow
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
@ -265,7 +267,10 @@ class BrowseSourceScreenModel(
|
|||||||
else -> {
|
else -> {
|
||||||
val preselectedIds = getCategories.await(manga.id).map { it.id }
|
val preselectedIds = getCategories.await(manga.id).map { it.id }
|
||||||
setDialog(
|
setDialog(
|
||||||
Dialog.ChangeMangaCategory(manga, categories.mapAsCheckboxState { it.id in preselectedIds }),
|
Dialog.ChangeMangaCategory(
|
||||||
|
manga,
|
||||||
|
categories.mapAsCheckboxState { it.id in preselectedIds }.toImmutableList(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -338,7 +343,7 @@ class BrowseSourceScreenModel(
|
|||||||
data class AddDuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog
|
data class AddDuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog
|
||||||
data class ChangeMangaCategory(
|
data class ChangeMangaCategory(
|
||||||
val manga: Manga,
|
val manga: Manga,
|
||||||
val initialSelection: List<CheckboxState.State<Category>>,
|
val initialSelection: ImmutableList<CheckboxState.State<Category>>,
|
||||||
) : Dialog
|
) : Dialog
|
||||||
data class Migrate(val newManga: Manga) : Dialog
|
data class Migrate(val newManga: Manga) : Dialog
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.util.fastMap
|
||||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
@ -15,6 +16,7 @@ import eu.kanade.presentation.category.components.CategoryRenameDialog
|
|||||||
import eu.kanade.presentation.category.components.CategorySortAlphabeticallyDialog
|
import eu.kanade.presentation.category.components.CategorySortAlphabeticallyDialog
|
||||||
import eu.kanade.presentation.util.Screen
|
import eu.kanade.presentation.util.Screen
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
|
|
||||||
@ -52,22 +54,22 @@ class CategoryScreen : Screen() {
|
|||||||
CategoryCreateDialog(
|
CategoryCreateDialog(
|
||||||
onDismissRequest = screenModel::dismissDialog,
|
onDismissRequest = screenModel::dismissDialog,
|
||||||
onCreate = screenModel::createCategory,
|
onCreate = screenModel::createCategory,
|
||||||
categories = successState.categories,
|
categories = successState.categories.fastMap { it.name }.toImmutableList(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is CategoryDialog.Rename -> {
|
is CategoryDialog.Rename -> {
|
||||||
CategoryRenameDialog(
|
CategoryRenameDialog(
|
||||||
onDismissRequest = screenModel::dismissDialog,
|
onDismissRequest = screenModel::dismissDialog,
|
||||||
onRename = { screenModel.renameCategory(dialog.category, it) },
|
onRename = { screenModel.renameCategory(dialog.category, it) },
|
||||||
categories = successState.categories,
|
categories = successState.categories.fastMap { it.name }.toImmutableList(),
|
||||||
category = dialog.category,
|
category = dialog.category.name,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is CategoryDialog.Delete -> {
|
is CategoryDialog.Delete -> {
|
||||||
CategoryDeleteDialog(
|
CategoryDeleteDialog(
|
||||||
onDismissRequest = screenModel::dismissDialog,
|
onDismissRequest = screenModel::dismissDialog,
|
||||||
onDelete = { screenModel.deleteCategory(dialog.category.id) },
|
onDelete = { screenModel.deleteCategory(dialog.category.id) },
|
||||||
category = dialog.category,
|
category = dialog.category.name,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is CategoryDialog.SortAlphabetically -> {
|
is CategoryDialog.SortAlphabetically -> {
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi.ui.home
|
package eu.kanade.tachiyomi.ui.home
|
||||||
|
|
||||||
import androidx.activity.compose.PredictiveBackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
|
||||||
import androidx.compose.animation.core.animate
|
|
||||||
import androidx.compose.animation.core.tween
|
|
||||||
import androidx.compose.animation.expandVertically
|
import androidx.compose.animation.expandVertically
|
||||||
import androidx.compose.animation.shrinkVertically
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.animation.togetherWith
|
import androidx.compose.animation.togetherWith
|
||||||
@ -26,20 +23,13 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.TransformOrigin
|
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
|
||||||
import androidx.compose.ui.semantics.contentDescription
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.util.fastForEach
|
import androidx.compose.ui.util.fastForEach
|
||||||
import androidx.compose.ui.util.lerp
|
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
|
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
|
||||||
@ -59,7 +49,6 @@ import kotlinx.coroutines.flow.collectLatest
|
|||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import soup.compose.material.motion.MotionConstants
|
|
||||||
import soup.compose.material.motion.animation.materialFadeThroughIn
|
import soup.compose.material.motion.animation.materialFadeThroughIn
|
||||||
import soup.compose.material.motion.animation.materialFadeThroughOut
|
import soup.compose.material.motion.animation.materialFadeThroughOut
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
@ -70,7 +59,6 @@ import tachiyomi.presentation.core.components.material.Scaffold
|
|||||||
import tachiyomi.presentation.core.i18n.pluralStringResource
|
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import kotlin.coroutines.cancellation.CancellationException
|
|
||||||
|
|
||||||
object HomeScreen : Screen() {
|
object HomeScreen : Screen() {
|
||||||
|
|
||||||
@ -92,8 +80,6 @@ object HomeScreen : Screen() {
|
|||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
var scale by remember { mutableFloatStateOf(1f) }
|
|
||||||
|
|
||||||
TabNavigator(
|
TabNavigator(
|
||||||
tab = LibraryTab,
|
tab = LibraryTab,
|
||||||
key = TabNavigatorKey,
|
key = TabNavigatorKey,
|
||||||
@ -132,11 +118,6 @@ object HomeScreen : Screen() {
|
|||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.graphicsLayer {
|
|
||||||
scaleX = scale
|
|
||||||
scaleY = scale
|
|
||||||
transformOrigin = TransformOrigin(0.5f, 1f)
|
|
||||||
}
|
|
||||||
.padding(contentPadding)
|
.padding(contentPadding)
|
||||||
.consumeWindowInsets(contentPadding),
|
.consumeWindowInsets(contentPadding),
|
||||||
) {
|
) {
|
||||||
@ -157,30 +138,10 @@ object HomeScreen : Screen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val goToLibraryTab = { tabNavigator.current = LibraryTab }
|
val goToLibraryTab = { tabNavigator.current = LibraryTab }
|
||||||
|
BackHandler(
|
||||||
var handlingBack by remember { mutableStateOf(false) }
|
enabled = tabNavigator.current != LibraryTab,
|
||||||
PredictiveBackHandler(enabled = handlingBack || tabNavigator.current != LibraryTab) { progress ->
|
onBack = goToLibraryTab,
|
||||||
handlingBack = true
|
)
|
||||||
val currentTab = tabNavigator.current
|
|
||||||
try {
|
|
||||||
progress.collect { backEvent ->
|
|
||||||
scale = lerp(1f, 0.92f, LinearOutSlowInEasing.transform(backEvent.progress))
|
|
||||||
tabNavigator.current = if (backEvent.progress > 0.25f) tabs[0] else currentTab
|
|
||||||
}
|
|
||||||
goToLibraryTab()
|
|
||||||
} catch (e: CancellationException) {
|
|
||||||
tabNavigator.current = currentTab
|
|
||||||
} finally {
|
|
||||||
animate(
|
|
||||||
initialValue = scale,
|
|
||||||
targetValue = 1f,
|
|
||||||
animationSpec = tween(durationMillis = MotionConstants.DefaultMotionDuration),
|
|
||||||
) { value, _ ->
|
|
||||||
scale = value
|
|
||||||
}
|
|
||||||
handlingBack = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
launch {
|
launch {
|
||||||
|
@ -28,9 +28,11 @@ import eu.kanade.tachiyomi.source.model.SManga
|
|||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.util.chapter.getNextUnread
|
import eu.kanade.tachiyomi.util.chapter.getNextUnread
|
||||||
import eu.kanade.tachiyomi.util.removeCovers
|
import eu.kanade.tachiyomi.util.removeCovers
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.PersistentList
|
import kotlinx.collections.immutable.PersistentList
|
||||||
import kotlinx.collections.immutable.mutate
|
import kotlinx.collections.immutable.mutate
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
@ -661,13 +663,15 @@ class LibraryScreenModel(
|
|||||||
val common = getCommonCategories(mangaList)
|
val common = getCommonCategories(mangaList)
|
||||||
// Get indexes of the mix categories to preselect.
|
// Get indexes of the mix categories to preselect.
|
||||||
val mix = getMixCategories(mangaList)
|
val mix = getMixCategories(mangaList)
|
||||||
val preselected = categories.map {
|
val preselected = categories
|
||||||
|
.map {
|
||||||
when (it) {
|
when (it) {
|
||||||
in common -> CheckboxState.State.Checked(it)
|
in common -> CheckboxState.State.Checked(it)
|
||||||
in mix -> CheckboxState.TriState.Exclude(it)
|
in mix -> CheckboxState.TriState.Exclude(it)
|
||||||
else -> CheckboxState.State.None(it)
|
else -> CheckboxState.State.None(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.toImmutableList()
|
||||||
mutableState.update { it.copy(dialog = Dialog.ChangeCategory(mangaList, preselected)) }
|
mutableState.update { it.copy(dialog = Dialog.ChangeCategory(mangaList, preselected)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -683,7 +687,10 @@ class LibraryScreenModel(
|
|||||||
|
|
||||||
sealed interface Dialog {
|
sealed interface Dialog {
|
||||||
data object SettingsSheet : Dialog
|
data object SettingsSheet : Dialog
|
||||||
data class ChangeCategory(val manga: List<Manga>, val initialSelection: List<CheckboxState<Category>>) : Dialog
|
data class ChangeCategory(
|
||||||
|
val manga: List<Manga>,
|
||||||
|
val initialSelection: ImmutableList<CheckboxState<Category>>,
|
||||||
|
) : Dialog
|
||||||
data class DeleteManga(val manga: List<Manga>) : Dialog
|
data class DeleteManga(val manga: List<Manga>) : Dialog
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ import android.os.Bundle
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
@ -64,7 +65,7 @@ import eu.kanade.tachiyomi.data.download.DownloadCache
|
|||||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
import eu.kanade.tachiyomi.data.updater.AppUpdateChecker
|
import eu.kanade.tachiyomi.data.updater.AppUpdateChecker
|
||||||
import eu.kanade.tachiyomi.data.updater.RELEASE_URL
|
import eu.kanade.tachiyomi.data.updater.RELEASE_URL
|
||||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
import eu.kanade.tachiyomi.extension.api.ExtensionApi
|
||||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
|
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen
|
||||||
@ -222,13 +223,14 @@ class MainActivity : BaseActivity() {
|
|||||||
contentWindowInsets = scaffoldInsets,
|
contentWindowInsets = scaffoldInsets,
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
// Consume insets already used by app state banners
|
// Consume insets already used by app state banners
|
||||||
// Shows current screen
|
Box(
|
||||||
DefaultNavigatorScreenTransition(
|
|
||||||
navigator = navigator,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(contentPadding)
|
.padding(contentPadding)
|
||||||
.consumeWindowInsets(contentPadding),
|
.consumeWindowInsets(contentPadding),
|
||||||
)
|
) {
|
||||||
|
// Shows current screen
|
||||||
|
DefaultNavigatorScreenTransition(navigator = navigator)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pop source-related screens when incognito mode is turned off
|
// Pop source-related screens when incognito mode is turned off
|
||||||
@ -335,7 +337,7 @@ class MainActivity : BaseActivity() {
|
|||||||
// Extensions updates
|
// Extensions updates
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
try {
|
try {
|
||||||
ExtensionGithubApi().checkForUpdates(context)
|
ExtensionApi().checkForUpdates(context)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, e)
|
logcat(LogPriority.ERROR, e)
|
||||||
}
|
}
|
||||||
|
@ -104,7 +104,7 @@ class MangaScreen(
|
|||||||
MangaScreen(
|
MangaScreen(
|
||||||
state = successState,
|
state = successState,
|
||||||
snackbarHostState = screenModel.snackbarHostState,
|
snackbarHostState = screenModel.snackbarHostState,
|
||||||
fetchInterval = successState.manga.fetchInterval,
|
nextUpdate = successState.manga.expectedNextUpdate,
|
||||||
isTabletUi = isTabletUi(),
|
isTabletUi = isTabletUi(),
|
||||||
chapterSwipeStartAction = screenModel.chapterSwipeStartAction,
|
chapterSwipeStartAction = screenModel.chapterSwipeStartAction,
|
||||||
chapterSwipeEndAction = screenModel.chapterSwipeEndAction,
|
chapterSwipeEndAction = screenModel.chapterSwipeEndAction,
|
||||||
@ -146,7 +146,7 @@ class MangaScreen(
|
|||||||
onDownloadActionClicked = screenModel::runDownloadAction.takeIf { !successState.source.isLocalOrStub() },
|
onDownloadActionClicked = screenModel::runDownloadAction.takeIf { !successState.source.isLocalOrStub() },
|
||||||
onEditCategoryClicked = screenModel::showChangeCategoryDialog.takeIf { successState.manga.favorite },
|
onEditCategoryClicked = screenModel::showChangeCategoryDialog.takeIf { successState.manga.favorite },
|
||||||
onEditFetchIntervalClicked = screenModel::showSetFetchIntervalDialog.takeIf {
|
onEditFetchIntervalClicked = screenModel::showSetFetchIntervalDialog.takeIf {
|
||||||
screenModel.isUpdateIntervalEnabled && successState.manga.favorite
|
successState.manga.favorite
|
||||||
},
|
},
|
||||||
onMigrateClicked = {
|
onMigrateClicked = {
|
||||||
navigator.push(MigrateSearchScreen(successState.manga.id))
|
navigator.push(MigrateSearchScreen(successState.manga.id))
|
||||||
@ -243,9 +243,10 @@ class MangaScreen(
|
|||||||
is MangaScreenModel.Dialog.SetFetchInterval -> {
|
is MangaScreenModel.Dialog.SetFetchInterval -> {
|
||||||
SetIntervalDialog(
|
SetIntervalDialog(
|
||||||
interval = dialog.manga.fetchInterval,
|
interval = dialog.manga.fetchInterval,
|
||||||
nextUpdate = dialog.manga.nextUpdate,
|
nextUpdate = dialog.manga.expectedNextUpdate,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
onValueChanged = { screenModel.setFetchInterval(dialog.manga, it) },
|
onValueChanged = { interval: Int -> screenModel.setFetchInterval(dialog.manga, interval) }
|
||||||
|
.takeIf { screenModel.isUpdateIntervalEnabled },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,8 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
|||||||
import eu.kanade.tachiyomi.util.chapter.getNextUnread
|
import eu.kanade.tachiyomi.util.chapter.getNextUnread
|
||||||
import eu.kanade.tachiyomi.util.removeCovers
|
import eu.kanade.tachiyomi.util.removeCovers
|
||||||
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
@ -360,7 +362,7 @@ class MangaScreenModel(
|
|||||||
successState.copy(
|
successState.copy(
|
||||||
dialog = Dialog.ChangeCategory(
|
dialog = Dialog.ChangeCategory(
|
||||||
manga = manga,
|
manga = manga,
|
||||||
initialSelection = categories.mapAsCheckboxState { it.id in selection },
|
initialSelection = categories.mapAsCheckboxState { it.id in selection }.toImmutableList(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -992,7 +994,10 @@ class MangaScreenModel(
|
|||||||
// Track sheet - end
|
// Track sheet - end
|
||||||
|
|
||||||
sealed interface Dialog {
|
sealed interface Dialog {
|
||||||
data class ChangeCategory(val manga: Manga, val initialSelection: List<CheckboxState<Category>>) : Dialog
|
data class ChangeCategory(
|
||||||
|
val manga: Manga,
|
||||||
|
val initialSelection: ImmutableList<CheckboxState<Category>>,
|
||||||
|
) : Dialog
|
||||||
data class DeleteChapters(val chapters: List<Chapter>) : Dialog
|
data class DeleteChapters(val chapters: List<Chapter>) : Dialog
|
||||||
data class DuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog
|
data class DuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog
|
||||||
data class SetFetchInterval(val manga: Manga) : Dialog
|
data class SetFetchInterval(val manga: Manga) : Dialog
|
||||||
|
@ -55,6 +55,7 @@ import kotlinx.coroutines.flow.update
|
|||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.preference.toggle
|
import tachiyomi.core.preference.toggle
|
||||||
|
import tachiyomi.core.storage.UniFileTempFileManager
|
||||||
import tachiyomi.core.util.lang.launchIO
|
import tachiyomi.core.util.lang.launchIO
|
||||||
import tachiyomi.core.util.lang.launchNonCancellable
|
import tachiyomi.core.util.lang.launchNonCancellable
|
||||||
import tachiyomi.core.util.lang.withIOContext
|
import tachiyomi.core.util.lang.withIOContext
|
||||||
@ -85,6 +86,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
private val sourceManager: SourceManager = Injekt.get(),
|
private val sourceManager: SourceManager = Injekt.get(),
|
||||||
private val downloadManager: DownloadManager = Injekt.get(),
|
private val downloadManager: DownloadManager = Injekt.get(),
|
||||||
private val downloadProvider: DownloadProvider = Injekt.get(),
|
private val downloadProvider: DownloadProvider = Injekt.get(),
|
||||||
|
private val tempFileManager: UniFileTempFileManager = Injekt.get(),
|
||||||
private val imageSaver: ImageSaver = Injekt.get(),
|
private val imageSaver: ImageSaver = Injekt.get(),
|
||||||
preferences: BasePreferences = Injekt.get(),
|
preferences: BasePreferences = Injekt.get(),
|
||||||
val readerPreferences: ReaderPreferences = Injekt.get(),
|
val readerPreferences: ReaderPreferences = Injekt.get(),
|
||||||
@ -269,7 +271,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
|
|
||||||
val context = Injekt.get<Application>()
|
val context = Injekt.get<Application>()
|
||||||
val source = sourceManager.getOrStub(manga.source)
|
val source = sourceManager.getOrStub(manga.source)
|
||||||
loader = ChapterLoader(context, downloadManager, downloadProvider, manga, source)
|
loader = ChapterLoader(context, downloadManager, downloadProvider, tempFileManager, manga, source)
|
||||||
|
|
||||||
loadChapter(loader!!, chapterList.first { chapterId == it.chapter.id })
|
loadChapter(loader!!, chapterList.first { chapterId == it.chapter.id })
|
||||||
Result.success(true)
|
Result.success(true)
|
||||||
@ -904,6 +906,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
private fun deletePendingChapters() {
|
private fun deletePendingChapters() {
|
||||||
viewModelScope.launchNonCancellable {
|
viewModelScope.launchNonCancellable {
|
||||||
downloadManager.deletePendingChapters()
|
downloadManager.deletePendingChapters()
|
||||||
|
tempFileManager.deleteTempFiles()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import eu.kanade.tachiyomi.source.Source
|
|||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||||
import tachiyomi.core.i18n.stringResource
|
import tachiyomi.core.i18n.stringResource
|
||||||
import tachiyomi.core.storage.toTempFile
|
import tachiyomi.core.storage.UniFileTempFileManager
|
||||||
import tachiyomi.core.util.lang.withIOContext
|
import tachiyomi.core.util.lang.withIOContext
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
@ -24,6 +24,7 @@ class ChapterLoader(
|
|||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val downloadManager: DownloadManager,
|
private val downloadManager: DownloadManager,
|
||||||
private val downloadProvider: DownloadProvider,
|
private val downloadProvider: DownloadProvider,
|
||||||
|
private val tempFileManager: UniFileTempFileManager,
|
||||||
private val manga: Manga,
|
private val manga: Manga,
|
||||||
private val source: Source,
|
private val source: Source,
|
||||||
) {
|
) {
|
||||||
@ -85,17 +86,24 @@ class ChapterLoader(
|
|||||||
skipCache = true,
|
skipCache = true,
|
||||||
)
|
)
|
||||||
return when {
|
return when {
|
||||||
isDownloaded -> DownloadPageLoader(chapter, manga, source, downloadManager, downloadProvider)
|
isDownloaded -> DownloadPageLoader(
|
||||||
|
chapter,
|
||||||
|
manga,
|
||||||
|
source,
|
||||||
|
downloadManager,
|
||||||
|
downloadProvider,
|
||||||
|
tempFileManager,
|
||||||
|
)
|
||||||
source is LocalSource -> source.getFormat(chapter.chapter).let { format ->
|
source is LocalSource -> source.getFormat(chapter.chapter).let { format ->
|
||||||
when (format) {
|
when (format) {
|
||||||
is Format.Directory -> DirectoryPageLoader(format.file)
|
is Format.Directory -> DirectoryPageLoader(format.file)
|
||||||
is Format.Zip -> ZipPageLoader(format.file.toTempFile(context))
|
is Format.Zip -> ZipPageLoader(tempFileManager.createTempFile(format.file))
|
||||||
is Format.Rar -> try {
|
is Format.Rar -> try {
|
||||||
RarPageLoader(format.file.toTempFile(context))
|
RarPageLoader(tempFileManager.createTempFile(format.file))
|
||||||
} catch (e: UnsupportedRarV5Exception) {
|
} catch (e: UnsupportedRarV5Exception) {
|
||||||
error(context.stringResource(MR.strings.loader_rar5_error))
|
error(context.stringResource(MR.strings.loader_rar5_error))
|
||||||
}
|
}
|
||||||
is Format.Epub -> EpubPageLoader(format.file.toTempFile(context))
|
is Format.Epub -> EpubPageLoader(tempFileManager.createTempFile(format.file))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
source is HttpSource -> HttpPageLoader(chapter, source)
|
source is HttpSource -> HttpPageLoader(chapter, source)
|
||||||
|
@ -10,7 +10,7 @@ import eu.kanade.tachiyomi.source.Source
|
|||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||||
import tachiyomi.core.storage.toTempFile
|
import tachiyomi.core.storage.UniFileTempFileManager
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
@ -23,6 +23,7 @@ internal class DownloadPageLoader(
|
|||||||
private val source: Source,
|
private val source: Source,
|
||||||
private val downloadManager: DownloadManager,
|
private val downloadManager: DownloadManager,
|
||||||
private val downloadProvider: DownloadProvider,
|
private val downloadProvider: DownloadProvider,
|
||||||
|
private val tempFileManager: UniFileTempFileManager,
|
||||||
) : PageLoader() {
|
) : PageLoader() {
|
||||||
|
|
||||||
private val context: Application by injectLazy()
|
private val context: Application by injectLazy()
|
||||||
@ -46,8 +47,8 @@ internal class DownloadPageLoader(
|
|||||||
zipPageLoader?.recycle()
|
zipPageLoader?.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getPagesFromArchive(chapterPath: UniFile): List<ReaderPage> {
|
private suspend fun getPagesFromArchive(file: UniFile): List<ReaderPage> {
|
||||||
val loader = ZipPageLoader(chapterPath.toTempFile(context)).also { zipPageLoader = it }
|
val loader = ZipPageLoader(tempFileManager.createTempFile(file)).also { zipPageLoader = it }
|
||||||
return loader.getPages()
|
return loader.getPages()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,7 +40,6 @@ class SettingsScreen(
|
|||||||
Destination.Tracking.id -> SettingsTrackingScreen
|
Destination.Tracking.id -> SettingsTrackingScreen
|
||||||
else -> SettingsMainScreen
|
else -> SettingsMainScreen
|
||||||
},
|
},
|
||||||
onBackPressed = null,
|
|
||||||
content = {
|
content = {
|
||||||
val pop: () -> Unit = {
|
val pop: () -> Unit = {
|
||||||
if (it.canPop) {
|
if (it.canPop) {
|
||||||
@ -62,7 +61,6 @@ class SettingsScreen(
|
|||||||
Destination.Tracking.id -> SettingsTrackingScreen
|
Destination.Tracking.id -> SettingsTrackingScreen
|
||||||
else -> SettingsAppearanceScreen
|
else -> SettingsAppearanceScreen
|
||||||
},
|
},
|
||||||
onBackPressed = null,
|
|
||||||
) {
|
) {
|
||||||
val insets = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal)
|
val insets = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal)
|
||||||
TwoPanelBox(
|
TwoPanelBox(
|
||||||
|
@ -4,11 +4,9 @@ package eu.kanade.tachiyomi.util.view
|
|||||||
|
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.os.Build
|
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.RoundedCorner
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
@ -97,22 +95,3 @@ fun View?.isVisibleOnScreen(): Boolean {
|
|||||||
Rect(0, 0, Resources.getSystem().displayMetrics.widthPixels, Resources.getSystem().displayMetrics.heightPixels)
|
Rect(0, 0, Resources.getSystem().displayMetrics.widthPixels, Resources.getSystem().displayMetrics.heightPixels)
|
||||||
return actualPosition.intersect(screen)
|
return actualPosition.intersect(screen)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns window radius (in pixel) applied to this view
|
|
||||||
*/
|
|
||||||
fun View.getWindowRadius(): Int {
|
|
||||||
val rad = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
val windowInsets = rootWindowInsets
|
|
||||||
listOfNotNull(
|
|
||||||
windowInsets.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT),
|
|
||||||
windowInsets.getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT),
|
|
||||||
windowInsets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT),
|
|
||||||
windowInsets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT),
|
|
||||||
)
|
|
||||||
.minOfOrNull { it.radius }
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
return rad ?: 0
|
|
||||||
}
|
|
||||||
|
@ -128,14 +128,13 @@ fun OkHttpClient.Builder.dohQuad101() = dns(
|
|||||||
/*
|
/*
|
||||||
* Mullvad DoH
|
* Mullvad DoH
|
||||||
* without ad blocking option
|
* without ad blocking option
|
||||||
* Source : https://mullvad.net/en/help/dns-over-https-and-dns-over-tls/
|
* Source: https://mullvad.net/en/help/dns-over-https-and-dns-over-tls
|
||||||
*/
|
*/
|
||||||
fun OkHttpClient.Builder.dohMullvad() = dns(
|
fun OkHttpClient.Builder.dohMullvad() = dns(
|
||||||
DnsOverHttps.Builder().client(build())
|
DnsOverHttps.Builder().client(build())
|
||||||
.url("https://doh.mullvad.net/dns-query".toHttpUrl())
|
.url(" https://dns.mullvad.net/dns-query".toHttpUrl())
|
||||||
.bootstrapDnsHosts(
|
.bootstrapDnsHosts(
|
||||||
InetAddress.getByName("194.242.2.2"),
|
InetAddress.getByName("194.242.2.2"),
|
||||||
InetAddress.getByName("193.19.108.2"),
|
|
||||||
InetAddress.getByName("2a07:e340::2"),
|
InetAddress.getByName("2a07:e340::2"),
|
||||||
)
|
)
|
||||||
.build(),
|
.build(),
|
||||||
@ -144,7 +143,7 @@ fun OkHttpClient.Builder.dohMullvad() = dns(
|
|||||||
/*
|
/*
|
||||||
* Control D
|
* Control D
|
||||||
* unfiltered option
|
* unfiltered option
|
||||||
* Source : https://controld.com/free-dns/?
|
* Source: https://controld.com/free-dns/?
|
||||||
*/
|
*/
|
||||||
fun OkHttpClient.Builder.dohControlD() = dns(
|
fun OkHttpClient.Builder.dohControlD() = dns(
|
||||||
DnsOverHttps.Builder().client(build())
|
DnsOverHttps.Builder().client(build())
|
||||||
|
@ -1,11 +1,6 @@
|
|||||||
package tachiyomi.core.storage
|
package tachiyomi.core.storage
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.FileUtils
|
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import java.io.BufferedOutputStream
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
val UniFile.extension: String?
|
val UniFile.extension: String?
|
||||||
get() = name?.substringAfterLast('.')
|
get() = name?.substringAfterLast('.')
|
||||||
@ -15,27 +10,3 @@ val UniFile.nameWithoutExtension: String?
|
|||||||
|
|
||||||
val UniFile.displayablePath: String
|
val UniFile.displayablePath: String
|
||||||
get() = filePath ?: uri.toString()
|
get() = filePath ?: uri.toString()
|
||||||
|
|
||||||
fun UniFile.toTempFile(context: Context): File {
|
|
||||||
val inputStream = context.contentResolver.openInputStream(uri)!!
|
|
||||||
val tempFile = File.createTempFile(
|
|
||||||
nameWithoutExtension.orEmpty().padEnd(3), // Prefix must be 3+ chars
|
|
||||||
null,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
FileUtils.copy(inputStream, tempFile.outputStream())
|
|
||||||
} else {
|
|
||||||
BufferedOutputStream(tempFile.outputStream()).use { tmpOut ->
|
|
||||||
inputStream.use { input ->
|
|
||||||
val buffer = ByteArray(8192)
|
|
||||||
var count: Int
|
|
||||||
while (input.read(buffer).also { count = it } > 0) {
|
|
||||||
tmpOut.write(buffer, 0, count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tempFile
|
|
||||||
}
|
|
||||||
|
@ -0,0 +1,46 @@
|
|||||||
|
package tachiyomi.core.storage
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.FileUtils
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
|
import java.io.BufferedOutputStream
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class UniFileTempFileManager(
|
||||||
|
private val context: Context,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val dir = File(context.externalCacheDir, "tmp")
|
||||||
|
|
||||||
|
fun createTempFile(file: UniFile): File {
|
||||||
|
dir.mkdirs()
|
||||||
|
|
||||||
|
val inputStream = context.contentResolver.openInputStream(file.uri)!!
|
||||||
|
val tempFile = File.createTempFile(
|
||||||
|
file.nameWithoutExtension.orEmpty().padEnd(3), // Prefix must be 3+ chars
|
||||||
|
null,
|
||||||
|
dir,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
FileUtils.copy(inputStream, tempFile.outputStream())
|
||||||
|
} else {
|
||||||
|
BufferedOutputStream(tempFile.outputStream()).use { tmpOut ->
|
||||||
|
inputStream.use { input ->
|
||||||
|
val buffer = ByteArray(8192)
|
||||||
|
var count: Int
|
||||||
|
while (input.read(buffer).also { count = it } > 0) {
|
||||||
|
tmpOut.write(buffer, 0, count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tempFile
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteTempFiles() {
|
||||||
|
dir.deleteRecursively()
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,10 @@
|
|||||||
package tachiyomi.domain.manga.model
|
package tachiyomi.domain.manga.model
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||||
import tachiyomi.core.preference.TriState
|
import tachiyomi.core.preference.TriState
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
data class Manga(
|
data class Manga(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
@ -29,6 +31,11 @@ data class Manga(
|
|||||||
val favoriteModifiedAt: Long?,
|
val favoriteModifiedAt: Long?,
|
||||||
) : Serializable {
|
) : Serializable {
|
||||||
|
|
||||||
|
val expectedNextUpdate: Instant?
|
||||||
|
get() = nextUpdate
|
||||||
|
.takeIf { status != SManga.COMPLETED.toLong() }
|
||||||
|
?.let { Instant.ofEpochMilli(it) }
|
||||||
|
|
||||||
val sorting: Long
|
val sorting: Long
|
||||||
get() = chapterFlags and CHAPTER_SORTING_MASK
|
get() = chapterFlags and CHAPTER_SORTING_MASK
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp_version = "8.2.0"
|
agp_version = "8.2.1"
|
||||||
lifecycle_version = "2.6.2"
|
lifecycle_version = "2.6.2"
|
||||||
paging_version = "3.2.1"
|
paging_version = "3.2.1"
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
[versions]
|
[versions]
|
||||||
aboutlib_version = "10.10.0"
|
aboutlib_version = "10.10.0"
|
||||||
acra = "5.11.3"
|
acra = "5.11.3"
|
||||||
leakcanary = "2.12"
|
leakcanary = "2.13"
|
||||||
moko = "0.23.0"
|
moko = "0.23.0"
|
||||||
okhttp_version = "5.0.0-alpha.12"
|
okhttp_version = "5.0.0-alpha.12"
|
||||||
richtext = "0.17.0"
|
richtext = "0.20.0"
|
||||||
shizuku_version = "12.2.0"
|
shizuku_version = "12.2.0"
|
||||||
sqldelight = "2.0.0"
|
sqldelight = "2.0.0"
|
||||||
sqlite = "2.4.0"
|
sqlite = "2.4.0"
|
||||||
|
@ -80,4 +80,9 @@
|
|||||||
<item quantity="one">Extension update available</item>
|
<item quantity="one">Extension update available</item>
|
||||||
<item quantity="other">%d extension updates available</item>
|
<item quantity="other">%d extension updates available</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
|
<plurals name="num_repos">
|
||||||
|
<item quantity="one">%d repo</item>
|
||||||
|
<item quantity="other">%d repos</item>
|
||||||
|
</plurals>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -164,7 +164,6 @@
|
|||||||
<string name="action_webview_forward">Forward</string>
|
<string name="action_webview_forward">Forward</string>
|
||||||
<string name="action_webview_refresh">Refresh</string>
|
<string name="action_webview_refresh">Refresh</string>
|
||||||
<string name="action_start_downloading_now">Start downloading now</string>
|
<string name="action_start_downloading_now">Start downloading now</string>
|
||||||
<string name="action_faq_and_guides">FAQ and Guides</string>
|
|
||||||
<string name="action_not_now">Not now</string>
|
<string name="action_not_now">Not now</string>
|
||||||
|
|
||||||
<!-- Operations -->
|
<!-- Operations -->
|
||||||
@ -281,12 +280,12 @@
|
|||||||
<string name="charging">When charging</string>
|
<string name="charging">When charging</string>
|
||||||
<string name="restrictions">Restrictions: %s</string>
|
<string name="restrictions">Restrictions: %s</string>
|
||||||
|
|
||||||
<string name="pref_library_update_manga_restriction">Skip updating entries</string>
|
<string name="pref_library_update_smart_update">Smart update</string>
|
||||||
<string name="pref_update_only_completely_read">With unread chapter(s)</string>
|
<string name="pref_update_only_completely_read">Skip entries with unread chapter(s)</string>
|
||||||
<string name="pref_update_only_non_completed">With \"Completed\" status</string>
|
<string name="pref_update_only_non_completed">Skip entries with \"Completed\" status</string>
|
||||||
<string name="pref_update_only_started">That haven\'t been started</string>
|
<string name="pref_update_only_started">Skip unstarted entries</string>
|
||||||
|
<string name="pref_update_only_in_release_period">Predict next release time</string>
|
||||||
<string name="pref_library_update_show_tab_badge">Show unread count on Updates icon</string>
|
<string name="pref_library_update_show_tab_badge">Show unread count on Updates icon</string>
|
||||||
<string name="pref_update_only_in_release_period">Outside expected release period</string>
|
|
||||||
|
|
||||||
<string name="pref_library_update_refresh_metadata">Automatically refresh metadata</string>
|
<string name="pref_library_update_refresh_metadata">Automatically refresh metadata</string>
|
||||||
<string name="pref_library_update_refresh_metadata_summary">Check for new cover and details when updating library</string>
|
<string name="pref_library_update_refresh_metadata_summary">Check for new cover and details when updating library</string>
|
||||||
@ -323,7 +322,7 @@
|
|||||||
<string name="ext_uninstall">Uninstall</string>
|
<string name="ext_uninstall">Uninstall</string>
|
||||||
<string name="ext_app_info">App info</string>
|
<string name="ext_app_info">App info</string>
|
||||||
<string name="untrusted_extension">Untrusted extension</string>
|
<string name="untrusted_extension">Untrusted extension</string>
|
||||||
<string name="untrusted_extension_message">This extension was signed with an untrusted certificate and wasn\'t activated.\n\nA malicious extension could read any stored login credentials or execute arbitrary code.\n\nBy trusting this certificate you accept these risks.</string>
|
<string name="untrusted_extension_message">This extension was signed by any unknown author and wasn\'t loaded.\n\nMalicious extensions can read any stored login credentials or execute arbitrary code.\n\nBy trusting this extension\'s certificate, you accept these risks.</string>
|
||||||
<string name="obsolete_extension_message">This extension is no longer available. It may not function properly and can cause issues with the app. Uninstalling it is recommended.</string>
|
<string name="obsolete_extension_message">This extension is no longer available. It may not function properly and can cause issues with the app. Uninstalling it is recommended.</string>
|
||||||
<string name="unofficial_extension_message">This extension is not from the official list.</string>
|
<string name="unofficial_extension_message">This extension is not from the official list.</string>
|
||||||
<string name="extension_api_error">Failed to get extensions list</string>
|
<string name="extension_api_error">Failed to get extensions list</string>
|
||||||
@ -342,6 +341,18 @@
|
|||||||
<string name="ext_installer_shizuku_stopped">Shizuku is not running</string>
|
<string name="ext_installer_shizuku_stopped">Shizuku is not running</string>
|
||||||
<string name="ext_installer_shizuku_unavailable_dialog">Install and start Shizuku to use Shizuku as extension installer.</string>
|
<string name="ext_installer_shizuku_unavailable_dialog">Install and start Shizuku to use Shizuku as extension installer.</string>
|
||||||
|
|
||||||
|
<!-- Extension repos -->
|
||||||
|
<string name="label_extension_repos">Extension repos</string>
|
||||||
|
<string name="information_empty_repos">You have no repos set.</string>
|
||||||
|
<string name="action_add_repo">Add repo</string>
|
||||||
|
<string name="label_add_repo_input">Repo URL</string>
|
||||||
|
<string name="action_add_repo_message">Add additional repos to Tachiyomi. This should be a URL that ends with \"index.min.json\".</string>
|
||||||
|
<string name="error_repo_exists">This repo already exists!</string>
|
||||||
|
<string name="action_delete_repo">Delete repo</string>
|
||||||
|
<string name="invalid_repo_name">Invalid repo URL</string>
|
||||||
|
<string name="delete_repo_confirmation">Do you wish to delete the repo \"%s\"?</string>
|
||||||
|
<string name="repo_extension_message">This extension is from an external repo. Tap to view the repo.</string>
|
||||||
|
|
||||||
<!-- Reader section -->
|
<!-- Reader section -->
|
||||||
<string name="pref_fullscreen">Fullscreen</string>
|
<string name="pref_fullscreen">Fullscreen</string>
|
||||||
<string name="pref_show_navigation_mode">Show tap zones overlay</string>
|
<string name="pref_show_navigation_mode">Show tap zones overlay</string>
|
||||||
@ -708,9 +719,10 @@
|
|||||||
<string name="display_mode_chapter">Chapter %1$s</string>
|
<string name="display_mode_chapter">Chapter %1$s</string>
|
||||||
<string name="manga_display_interval_title">Estimate every</string>
|
<string name="manga_display_interval_title">Estimate every</string>
|
||||||
<string name="manga_display_modified_interval_title">Set to update every</string>
|
<string name="manga_display_modified_interval_title">Set to update every</string>
|
||||||
|
<string name="manga_interval_header">Next update</string>
|
||||||
<!-- "... around 2 days" -->
|
<!-- "... around 2 days" -->
|
||||||
<string name="manga_interval_expected_update">Next update expected in around %s</string>
|
<string name="manga_interval_expected_update">Next update expected in around %1$s, checking around every %2$s</string>
|
||||||
<string name="manga_modify_calculated_interval_title">Customize interval</string>
|
<string name="manga_interval_custom_amount">Custom update frequency:</string>
|
||||||
<string name="chapter_downloading_progress">Downloading (%1$d/%2$d)</string>
|
<string name="chapter_downloading_progress">Downloading (%1$d/%2$d)</string>
|
||||||
<string name="chapter_error">Error</string>
|
<string name="chapter_error">Error</string>
|
||||||
<string name="chapter_paused">Paused</string>
|
<string name="chapter_paused">Paused</string>
|
||||||
|
@ -1,11 +1,7 @@
|
|||||||
package tachiyomi.presentation.core.components
|
package tachiyomi.presentation.core.components
|
||||||
|
|
||||||
import androidx.activity.compose.PredictiveBackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
|
||||||
import androidx.compose.animation.core.Spring
|
|
||||||
import androidx.compose.animation.core.animate
|
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.core.spring
|
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.gestures.AnchoredDraggableState
|
import androidx.compose.foundation.gestures.AnchoredDraggableState
|
||||||
@ -30,7 +26,6 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@ -39,11 +34,8 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.composed
|
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.TransformOrigin
|
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
@ -53,14 +45,14 @@ import androidx.compose.ui.unit.Dp
|
|||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.Velocity
|
import androidx.compose.ui.unit.Velocity
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.util.lerp
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.drop
|
import kotlinx.coroutines.flow.drop
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.coroutines.cancellation.CancellationException
|
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
private val sheetAnimationSpec = tween<Float>(durationMillis = 350)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AdaptiveSheet(
|
fun AdaptiveSheet(
|
||||||
isTabletUi: Boolean,
|
isTabletUi: Boolean,
|
||||||
@ -99,11 +91,6 @@ fun AdaptiveSheet(
|
|||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.predictiveBackAnimation(
|
|
||||||
enabled = remember { derivedStateOf { alpha > 0f } }.value,
|
|
||||||
transformOrigin = TransformOrigin.Center,
|
|
||||||
onBack = internalOnDismissRequest,
|
|
||||||
)
|
|
||||||
.requiredWidthIn(max = 460.dp)
|
.requiredWidthIn(max = 460.dp)
|
||||||
.clickable(
|
.clickable(
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
@ -116,6 +103,7 @@ fun AdaptiveSheet(
|
|||||||
shape = MaterialTheme.shapes.extraLarge,
|
shape = MaterialTheme.shapes.extraLarge,
|
||||||
tonalElevation = tonalElevation,
|
tonalElevation = tonalElevation,
|
||||||
content = {
|
content = {
|
||||||
|
BackHandler(enabled = alpha > 0f, onBack = internalOnDismissRequest)
|
||||||
content()
|
content()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -157,11 +145,6 @@ fun AdaptiveSheet(
|
|||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.predictiveBackAnimation(
|
|
||||||
enabled = anchoredDraggableState.targetValue == 0,
|
|
||||||
transformOrigin = TransformOrigin(0.5f, 1f),
|
|
||||||
onBack = internalOnDismissRequest,
|
|
||||||
)
|
|
||||||
.widthIn(max = 460.dp)
|
.widthIn(max = 460.dp)
|
||||||
.clickable(
|
.clickable(
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
@ -201,6 +184,10 @@ fun AdaptiveSheet(
|
|||||||
shape = MaterialTheme.shapes.extraLarge,
|
shape = MaterialTheme.shapes.extraLarge,
|
||||||
tonalElevation = tonalElevation,
|
tonalElevation = tonalElevation,
|
||||||
content = {
|
content = {
|
||||||
|
BackHandler(
|
||||||
|
enabled = anchoredDraggableState.targetValue == 0,
|
||||||
|
onBack = internalOnDismissRequest,
|
||||||
|
)
|
||||||
content()
|
content()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -270,37 +257,3 @@ private fun <T> AnchoredDraggableState<T>.preUpPostDownNestedScrollConnection()
|
|||||||
@JvmName("offsetToFloat")
|
@JvmName("offsetToFloat")
|
||||||
private fun Offset.toFloat(): Float = this.y
|
private fun Offset.toFloat(): Float = this.y
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Modifier.predictiveBackAnimation(
|
|
||||||
enabled: Boolean,
|
|
||||||
transformOrigin: TransformOrigin,
|
|
||||||
onBack: () -> Unit,
|
|
||||||
) = composed {
|
|
||||||
var scale by remember { mutableFloatStateOf(1f) }
|
|
||||||
PredictiveBackHandler(enabled = enabled) { progress ->
|
|
||||||
try {
|
|
||||||
progress.collect { backEvent ->
|
|
||||||
scale = lerp(1f, 0.85f, LinearOutSlowInEasing.transform(backEvent.progress))
|
|
||||||
}
|
|
||||||
// Completion
|
|
||||||
onBack()
|
|
||||||
} catch (e: CancellationException) {
|
|
||||||
// Cancellation
|
|
||||||
} finally {
|
|
||||||
animate(
|
|
||||||
initialValue = scale,
|
|
||||||
targetValue = 1f,
|
|
||||||
animationSpec = spring(stiffness = Spring.StiffnessLow),
|
|
||||||
) { value, _ ->
|
|
||||||
scale = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Modifier.graphicsLayer {
|
|
||||||
this.scaleX = scale
|
|
||||||
this.scaleY = scale
|
|
||||||
this.transformOrigin = transformOrigin
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val sheetAnimationSpec = tween<Float>(durationMillis = 350)
|
|
||||||
|
@ -24,9 +24,9 @@ import tachiyomi.core.metadata.comicinfo.ComicInfo
|
|||||||
import tachiyomi.core.metadata.comicinfo.copyFromComicInfo
|
import tachiyomi.core.metadata.comicinfo.copyFromComicInfo
|
||||||
import tachiyomi.core.metadata.comicinfo.getComicInfo
|
import tachiyomi.core.metadata.comicinfo.getComicInfo
|
||||||
import tachiyomi.core.metadata.tachiyomi.MangaDetails
|
import tachiyomi.core.metadata.tachiyomi.MangaDetails
|
||||||
|
import tachiyomi.core.storage.UniFileTempFileManager
|
||||||
import tachiyomi.core.storage.extension
|
import tachiyomi.core.storage.extension
|
||||||
import tachiyomi.core.storage.nameWithoutExtension
|
import tachiyomi.core.storage.nameWithoutExtension
|
||||||
import tachiyomi.core.storage.toTempFile
|
|
||||||
import tachiyomi.core.util.lang.withIOContext
|
import tachiyomi.core.util.lang.withIOContext
|
||||||
import tachiyomi.core.util.system.ImageUtil
|
import tachiyomi.core.util.system.ImageUtil
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
@ -56,6 +56,7 @@ actual class LocalSource(
|
|||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
private val xml: XML by injectLazy()
|
private val xml: XML by injectLazy()
|
||||||
|
private val tempFileManager: UniFileTempFileManager by injectLazy()
|
||||||
|
|
||||||
private val POPULAR_FILTERS = FilterList(OrderBy.Popular(context))
|
private val POPULAR_FILTERS = FilterList(OrderBy.Popular(context))
|
||||||
private val LATEST_FILTERS = FilterList(OrderBy.Latest(context))
|
private val LATEST_FILTERS = FilterList(OrderBy.Latest(context))
|
||||||
@ -213,7 +214,7 @@ actual class LocalSource(
|
|||||||
for (chapter in chapterArchives) {
|
for (chapter in chapterArchives) {
|
||||||
when (Format.valueOf(chapter)) {
|
when (Format.valueOf(chapter)) {
|
||||||
is Format.Zip -> {
|
is Format.Zip -> {
|
||||||
ZipFile(chapter.toTempFile(context)).use { zip: ZipFile ->
|
ZipFile(tempFileManager.createTempFile(chapter)).use { zip: ZipFile ->
|
||||||
zip.getEntry(COMIC_INFO_FILE)?.let { comicInfoFile ->
|
zip.getEntry(COMIC_INFO_FILE)?.let { comicInfoFile ->
|
||||||
zip.getInputStream(comicInfoFile).buffered().use { stream ->
|
zip.getInputStream(comicInfoFile).buffered().use { stream ->
|
||||||
return copyComicInfoFile(stream, folderPath)
|
return copyComicInfoFile(stream, folderPath)
|
||||||
@ -222,7 +223,7 @@ actual class LocalSource(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
is Format.Rar -> {
|
is Format.Rar -> {
|
||||||
JunrarArchive(chapter.toTempFile(context)).use { rar ->
|
JunrarArchive(tempFileManager.createTempFile(chapter)).use { rar ->
|
||||||
rar.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE }?.let { comicInfoFile ->
|
rar.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE }?.let { comicInfoFile ->
|
||||||
rar.getInputStream(comicInfoFile).buffered().use { stream ->
|
rar.getInputStream(comicInfoFile).buffered().use { stream ->
|
||||||
return copyComicInfoFile(stream, folderPath)
|
return copyComicInfoFile(stream, folderPath)
|
||||||
@ -272,7 +273,7 @@ actual class LocalSource(
|
|||||||
|
|
||||||
val format = Format.valueOf(chapterFile)
|
val format = Format.valueOf(chapterFile)
|
||||||
if (format is Format.Epub) {
|
if (format is Format.Epub) {
|
||||||
EpubFile(format.file.toTempFile(context)).use { epub ->
|
EpubFile(tempFileManager.createTempFile(format.file)).use { epub ->
|
||||||
epub.fillMetadata(manga, this)
|
epub.fillMetadata(manga, this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -331,7 +332,7 @@ actual class LocalSource(
|
|||||||
entry?.let { coverManager.update(manga, it.openInputStream()) }
|
entry?.let { coverManager.update(manga, it.openInputStream()) }
|
||||||
}
|
}
|
||||||
is Format.Zip -> {
|
is Format.Zip -> {
|
||||||
ZipFile(format.file.toTempFile(context)).use { zip ->
|
ZipFile(tempFileManager.createTempFile(format.file)).use { zip ->
|
||||||
val entry = zip.entries().toList()
|
val entry = zip.entries().toList()
|
||||||
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||||
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
|
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
|
||||||
@ -340,7 +341,7 @@ actual class LocalSource(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
is Format.Rar -> {
|
is Format.Rar -> {
|
||||||
JunrarArchive(format.file.toTempFile(context)).use { archive ->
|
JunrarArchive(tempFileManager.createTempFile(format.file)).use { archive ->
|
||||||
val entry = archive.fileHeaders
|
val entry = archive.fileHeaders
|
||||||
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
|
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
|
||||||
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
|
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
|
||||||
@ -349,7 +350,7 @@ actual class LocalSource(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
is Format.Epub -> {
|
is Format.Epub -> {
|
||||||
EpubFile(format.file.toTempFile(context)).use { epub ->
|
EpubFile(tempFileManager.createTempFile(format.file)).use { epub ->
|
||||||
val entry = epub.getImagesFromPages()
|
val entry = epub.getImagesFromPages()
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
?.let { epub.getEntry(it) }
|
?.let { epub.getEntry(it) }
|
||||||
|
Loading…
Reference in New Issue
Block a user