mirror of
https://github.com/mihonapp/mihon.git
synced 2025-10-15 07:39:42 +02:00
chore: merge upstream changes.
Signed-off-by: KaiserBh <kaiserbh@proton.me>
This commit is contained in:
@@ -22,7 +22,7 @@ android {
|
||||
defaultConfig {
|
||||
applicationId = "eu.kanade.tachiyomi"
|
||||
|
||||
versionCode = 110
|
||||
versionCode = 111
|
||||
versionName = "0.14.7"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
@@ -196,7 +196,6 @@ dependencies {
|
||||
|
||||
// RxJava
|
||||
implementation(libs.rxjava)
|
||||
implementation(libs.flowreactivenetwork)
|
||||
|
||||
// Networking
|
||||
implementation(libs.bundles.okhttp)
|
||||
|
@@ -7,6 +7,9 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
|
||||
<!-- Storage -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<!-- For background jobs -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
@@ -21,6 +24,8 @@
|
||||
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.READ_APP_SPECIFIC_LOCALES" />
|
||||
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
|
||||
<!-- Remove permission from Firebase dependency -->
|
||||
<uses-permission android:name="com.google.android.gms.permission.AD_ID"
|
||||
@@ -36,6 +41,8 @@
|
||||
android:largeHeap="true"
|
||||
android:localeConfig="@xml/locales_config"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:preserveLegacyExternalStorage="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Tachiyomi">
|
||||
@@ -151,10 +158,6 @@
|
||||
android:name=".data.notification.NotificationReceiver"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".data.download.DownloadService"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".extension.util.ExtensionInstallService"
|
||||
android:exported="false" />
|
||||
@@ -168,6 +171,11 @@
|
||||
android:value="true" />
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
tools:node="merge" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
|
@@ -190,6 +190,7 @@ private fun ExtensionDetails(
|
||||
key = { it.source.id },
|
||||
) { source ->
|
||||
SourceSwitchPreference(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
source = source,
|
||||
onClickSourcePreferences = onClickSourcePreferences,
|
||||
onClickSource = onClickSource,
|
||||
|
@@ -58,6 +58,7 @@ private fun ExtensionFilterContent(
|
||||
) {
|
||||
items(state.languages) { language ->
|
||||
SwitchPreferenceWidget(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
title = LocaleHelper.getSourceDisplayName(language, context),
|
||||
checked = language in state.enabledLanguages,
|
||||
onCheckedChanged = { onClickLang(language) },
|
||||
|
@@ -148,12 +148,14 @@ private fun ExtensionContent(
|
||||
}
|
||||
ExtensionHeader(
|
||||
textRes = header.textRes,
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
action = action,
|
||||
)
|
||||
}
|
||||
is ExtensionUiModel.Header.Text -> {
|
||||
ExtensionHeader(
|
||||
text = header.text,
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -165,6 +167,7 @@ private fun ExtensionContent(
|
||||
key = { "extension-${it.hashCode()}" },
|
||||
) { item ->
|
||||
ExtensionItem(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
item = item,
|
||||
onClickItem = {
|
||||
when (it) {
|
||||
|
@@ -132,6 +132,7 @@ private fun MigrateSourceList(
|
||||
key = { (source, _) -> "migrate-${source.id}" },
|
||||
) { (source, count) ->
|
||||
MigrateSourceItem(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
source = source,
|
||||
count = count,
|
||||
onClickItem = { onClickItem(source) },
|
||||
|
@@ -68,6 +68,7 @@ private fun SourcesFilterContent(
|
||||
contentType = "source-filter-header",
|
||||
) {
|
||||
SourcesFilterHeader(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
language = language,
|
||||
enabled = enabled,
|
||||
onClickItem = onClickLanguage,
|
||||
@@ -80,6 +81,7 @@ private fun SourcesFilterContent(
|
||||
contentType = { "source-filter-item" },
|
||||
) { source ->
|
||||
SourcesFilterItem(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
source = source,
|
||||
enabled = "${source.id}" !in state.disabledSources,
|
||||
onClickItem = onClickSource,
|
||||
|
@@ -74,10 +74,12 @@ fun SourcesScreen(
|
||||
when (model) {
|
||||
is SourceUiModel.Header -> {
|
||||
SourceHeader(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
language = model.language,
|
||||
)
|
||||
}
|
||||
is SourceUiModel.Item -> SourceItem(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
source = model.source,
|
||||
onClickItem = onClickItem,
|
||||
onLongClickItem = onLongClickItem,
|
||||
|
@@ -107,6 +107,7 @@ private fun CategoryContent(
|
||||
key = { _, category -> "category-${category.id}" },
|
||||
) { index, category ->
|
||||
CategoryListItem(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
category = category,
|
||||
canMoveUp = index != 0,
|
||||
canMoveDown = index != categories.lastIndex,
|
||||
|
@@ -123,6 +123,7 @@ private fun HistoryScreenContent(
|
||||
when (item) {
|
||||
is HistoryUiModel.Header -> {
|
||||
RelativeDateHeader(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
date = item.date,
|
||||
relativeTime = relativeTime,
|
||||
dateFormat = dateFormat,
|
||||
@@ -131,6 +132,7 @@ private fun HistoryScreenContent(
|
||||
is HistoryUiModel.Item -> {
|
||||
val value = item.item
|
||||
HistoryItem(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
history = value,
|
||||
onClickCover = { onClickCover(value) },
|
||||
onClickResume = { onClickResume(value) },
|
||||
|
@@ -114,7 +114,8 @@ object SettingsDataScreen : SearchableSettings {
|
||||
return Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.pref_storage_location),
|
||||
subtitle = remember(storageDir) {
|
||||
(UniFile.fromUri(context, storageDir.toUri())?.filePath)
|
||||
val file = UniFile.fromUri(context, storageDir.toUri())
|
||||
file?.filePath ?: file?.uri?.toString()
|
||||
} ?: stringResource(MR.strings.invalid_location, storageDir),
|
||||
onClick = {
|
||||
try {
|
||||
|
@@ -13,7 +13,6 @@ import eu.kanade.presentation.more.settings.Preference
|
||||
import eu.kanade.presentation.more.settings.PreferenceScaffold
|
||||
import eu.kanade.presentation.more.settings.screen.about.AboutScreen
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||
import kotlinx.coroutines.guava.await
|
||||
|
@@ -18,17 +18,18 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import androidx.lifecycle.asFlow
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkQuery
|
||||
import cafe.adriel.voyager.core.model.ScreenModel
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.AppBarActions
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.presentation.util.ioCoroutineScope
|
||||
import eu.kanade.tachiyomi.util.lang.toDateTimestampString
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import eu.kanade.tachiyomi.util.system.workManager
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
@@ -39,6 +40,9 @@ import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.plus
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.Date
|
||||
|
||||
class WorkerInfoScreen : Screen() {
|
||||
|
||||
@@ -116,22 +120,19 @@ class WorkerInfoScreen : Screen() {
|
||||
private val workManager = context.workManager
|
||||
|
||||
val finished = workManager
|
||||
.getWorkInfosLiveData(
|
||||
.getWorkInfosFlow(
|
||||
WorkQuery.fromStates(WorkInfo.State.SUCCEEDED, WorkInfo.State.FAILED, WorkInfo.State.CANCELLED),
|
||||
)
|
||||
.asFlow()
|
||||
.map(::constructString)
|
||||
.stateIn(ioCoroutineScope, SharingStarted.WhileSubscribed(), "")
|
||||
|
||||
val running = workManager
|
||||
.getWorkInfosLiveData(WorkQuery.fromStates(WorkInfo.State.RUNNING))
|
||||
.asFlow()
|
||||
.getWorkInfosFlow(WorkQuery.fromStates(WorkInfo.State.RUNNING))
|
||||
.map(::constructString)
|
||||
.stateIn(ioCoroutineScope, SharingStarted.WhileSubscribed(), "")
|
||||
|
||||
val enqueued = workManager
|
||||
.getWorkInfosLiveData(WorkQuery.fromStates(WorkInfo.State.ENQUEUED))
|
||||
.asFlow()
|
||||
.getWorkInfosFlow(WorkQuery.fromStates(WorkInfo.State.ENQUEUED))
|
||||
.map(::constructString)
|
||||
.stateIn(ioCoroutineScope, SharingStarted.WhileSubscribed(), "")
|
||||
|
||||
@@ -146,6 +147,16 @@ class WorkerInfoScreen : Screen() {
|
||||
appendLine(" - $it")
|
||||
}
|
||||
appendLine("State: ${workInfo.state}")
|
||||
if (workInfo.state == WorkInfo.State.ENQUEUED) {
|
||||
appendLine(
|
||||
"Next scheduled run: ${Date(workInfo.nextScheduleTimeMillis).toDateTimestampString(
|
||||
UiPreferences.dateFormat(
|
||||
Injekt.get<UiPreferences>().dateFormat().get(),
|
||||
),
|
||||
)}",
|
||||
)
|
||||
appendLine("Attempt #${workInfo.runAttemptCount + 1}")
|
||||
}
|
||||
appendLine()
|
||||
}
|
||||
}
|
||||
|
@@ -1,27 +0,0 @@
|
||||
package eu.kanade.presentation.reader
|
||||
|
||||
import androidx.annotation.IntRange
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import kotlin.math.abs
|
||||
|
||||
@Composable
|
||||
fun BrightnessOverlay(
|
||||
@IntRange(from = -100, to = 100) value: Int,
|
||||
) {
|
||||
if (value >= 0) return
|
||||
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.graphicsLayer {
|
||||
alpha = abs(value) / 100f
|
||||
},
|
||||
) {
|
||||
drawRect(Color.Black)
|
||||
}
|
||||
}
|
@@ -11,8 +11,6 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.domain.manga.model.readerOrientation
|
||||
@@ -72,7 +70,7 @@ private fun DialogContent(
|
||||
selected = mode
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
imageVector = ImageVector.vectorResource(mode.iconRes),
|
||||
imageVector = mode.icon,
|
||||
title = stringResource(mode.stringRes),
|
||||
)
|
||||
}
|
||||
|
@@ -0,0 +1,49 @@
|
||||
package eu.kanade.presentation.reader
|
||||
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.IntRange
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import kotlin.math.abs
|
||||
|
||||
@Composable
|
||||
fun ReaderContentOverlay(
|
||||
@IntRange(from = -100, to = 100) brightness: Int,
|
||||
@ColorInt color: Int?,
|
||||
colorBlendMode: BlendMode?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (brightness < 0) {
|
||||
val brightnessAlpha = remember(brightness) {
|
||||
abs(brightness) / 100f
|
||||
}
|
||||
|
||||
Canvas(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.graphicsLayer {
|
||||
alpha = brightnessAlpha
|
||||
},
|
||||
) {
|
||||
drawRect(Color.Black)
|
||||
}
|
||||
}
|
||||
|
||||
if (color != null) {
|
||||
Canvas(
|
||||
modifier = modifier
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
drawRect(
|
||||
color = Color(color),
|
||||
blendMode = colorBlendMode ?: BlendMode.SrcOver,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@@ -49,7 +49,7 @@ fun BottomReaderBar(
|
||||
|
||||
IconButton(onClick = onClickOrientation) {
|
||||
Icon(
|
||||
painter = painterResource(orientation.iconRes),
|
||||
imageVector = orientation.icon,
|
||||
contentDescription = stringResource(MR.strings.rotation_type),
|
||||
)
|
||||
}
|
||||
|
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.presentation.reader.settings
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.Text
|
||||
@@ -10,6 +9,7 @@ import androidx.core.graphics.alpha
|
||||
import androidx.core.graphics.blue
|
||||
import androidx.core.graphics.green
|
||||
import androidx.core.graphics.red
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences.Companion.ColorFilterMode
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
||||
import tachiyomi.core.preference.getAndSet
|
||||
import tachiyomi.i18n.MR
|
||||
@@ -21,25 +21,6 @@ import tachiyomi.presentation.core.util.collectAsState
|
||||
|
||||
@Composable
|
||||
internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel) {
|
||||
val colorFilterModes = buildList {
|
||||
addAll(
|
||||
listOf(
|
||||
MR.strings.label_default,
|
||||
MR.strings.filter_mode_multiply,
|
||||
MR.strings.filter_mode_screen,
|
||||
),
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
addAll(
|
||||
listOf(
|
||||
MR.strings.filter_mode_overlay,
|
||||
MR.strings.filter_mode_lighten,
|
||||
MR.strings.filter_mode_darken,
|
||||
),
|
||||
)
|
||||
}
|
||||
}.map { stringResource(it) }
|
||||
|
||||
val customBrightness by screenModel.preferences.customBrightness().collectAsState()
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_custom_brightness),
|
||||
@@ -118,11 +99,11 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
|
||||
|
||||
val colorFilterMode by screenModel.preferences.colorFilterMode().collectAsState()
|
||||
SettingsChipRow(MR.strings.pref_color_filter_mode) {
|
||||
colorFilterModes.mapIndexed { index, it ->
|
||||
ColorFilterMode.mapIndexed { index, it ->
|
||||
FilterChip(
|
||||
selected = colorFilterMode == index,
|
||||
onClick = { screenModel.preferences.colorFilterMode().set(index) },
|
||||
label = { Text(it) },
|
||||
label = { Text(stringResource(it.first)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -193,6 +193,7 @@ fun TrackerSearch(
|
||||
type = it.publishing_type.toLowerCase(Locale.current).capitalize(Locale.current),
|
||||
startDate = it.start_date,
|
||||
status = it.publishing_status.toLowerCase(Locale.current).capitalize(Locale.current),
|
||||
score = it.score,
|
||||
description = it.summary.trim(),
|
||||
selected = it == selected,
|
||||
onClick = { onSelectedChange(it) },
|
||||
@@ -218,6 +219,7 @@ private fun SearchResultItem(
|
||||
type: String,
|
||||
startDate: String,
|
||||
status: String,
|
||||
score: Float,
|
||||
description: String,
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
@@ -279,6 +281,12 @@ private fun SearchResultItem(
|
||||
text = status,
|
||||
)
|
||||
}
|
||||
if (score != -1f) {
|
||||
SearchResultItemDetails(
|
||||
title = stringResource(MR.strings.score),
|
||||
text = score.toString(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (description.isNotBlank()) {
|
||||
|
@@ -53,6 +53,7 @@ internal fun LazyListScope.updatesLastUpdatedItem(
|
||||
item(key = "updates-lastUpdated") {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.animateItemPlacement()
|
||||
.padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small),
|
||||
) {
|
||||
Text(
|
||||
@@ -89,12 +90,14 @@ internal fun LazyListScope.updatesUiItems(
|
||||
when (item) {
|
||||
is UpdatesUiModel.Header -> {
|
||||
ListGroupHeader(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
text = item.date,
|
||||
)
|
||||
}
|
||||
is UpdatesUiModel.Item -> {
|
||||
val updatesItem = item.item
|
||||
UpdatesUiItem(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
update = updatesItem.update,
|
||||
selected = updatesItem.selected,
|
||||
readProgress = updatesItem.update.lastPageRead
|
||||
|
@@ -416,6 +416,11 @@ object Migrations {
|
||||
newKey = { Preference.appStateKey(it) },
|
||||
)
|
||||
}
|
||||
if (oldVersion < 111) {
|
||||
File(context.cacheDir, "dl_index_cache")
|
||||
.takeIf { it.exists() }
|
||||
?.delete()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
@@ -17,6 +17,7 @@ import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.util.system.cancelNotification
|
||||
import eu.kanade.tachiyomi.util.system.isRunning
|
||||
import eu.kanade.tachiyomi.util.system.setForegroundSafely
|
||||
import eu.kanade.tachiyomi.util.system.workManager
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.system.logcat
|
||||
@@ -39,19 +40,14 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
|
||||
|
||||
if (isAutoBackup && BackupRestoreJob.isRunning(context)) return Result.retry()
|
||||
|
||||
val backupPreferences = Injekt.get<BackupPreferences>()
|
||||
|
||||
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
|
||||
?: getAutomaticBackupLocation()
|
||||
?: return Result.failure()
|
||||
|
||||
val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupCreateFlags.AutomaticDefaults)
|
||||
setForegroundSafely()
|
||||
|
||||
try {
|
||||
setForeground(getForegroundInfo())
|
||||
} catch (e: IllegalStateException) {
|
||||
logcat(LogPriority.ERROR, e) { "Not allowed to run on foreground service" }
|
||||
}
|
||||
val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupCreateFlags.AutomaticDefaults)
|
||||
val backupPreferences = Injekt.get<BackupPreferences>()
|
||||
|
||||
return try {
|
||||
val location = BackupCreator(context).createBackup(uri, flags, isAutoBackup)
|
||||
|
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.data.backup
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.hippo.unifile.UniFile
|
||||
@@ -31,7 +30,6 @@ import eu.kanade.tachiyomi.data.backup.models.backupTrackMapper
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.preferenceKey
|
||||
import eu.kanade.tachiyomi.source.sourcePreferences
|
||||
import eu.kanade.tachiyomi.util.system.hasPermission
|
||||
import kotlinx.serialization.protobuf.ProtoBuf
|
||||
import logcat.LogPriority
|
||||
import okio.buffer
|
||||
@@ -73,10 +71,6 @@ class BackupCreator(
|
||||
* @param isAutoBackup backup called from scheduled backup job
|
||||
*/
|
||||
suspend fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String {
|
||||
if (!context.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||
throw IllegalStateException(context.stringResource(MR.strings.missing_storage_permission))
|
||||
}
|
||||
|
||||
val databaseManga = getFavorites.await()
|
||||
val backup = Backup(
|
||||
backupMangas(databaseManga, flags),
|
||||
|
@@ -79,11 +79,7 @@ class BackupNotifier(private val context: Context) {
|
||||
addAction(
|
||||
R.drawable.ic_share_24dp,
|
||||
context.stringResource(MR.strings.action_share),
|
||||
NotificationReceiver.shareBackupPendingBroadcast(
|
||||
context,
|
||||
unifile.uri,
|
||||
Notifications.ID_BACKUP_COMPLETE,
|
||||
),
|
||||
NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri),
|
||||
)
|
||||
|
||||
show(Notifications.ID_BACKUP_COMPLETE)
|
||||
|
@@ -12,6 +12,7 @@ import androidx.work.workDataOf
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.util.system.cancelNotification
|
||||
import eu.kanade.tachiyomi.util.system.isRunning
|
||||
import eu.kanade.tachiyomi.util.system.setForegroundSafely
|
||||
import eu.kanade.tachiyomi.util.system.workManager
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import logcat.LogPriority
|
||||
@@ -29,11 +30,7 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet
|
||||
?: return Result.failure()
|
||||
val sync = inputData.getBoolean(SYNC_KEY, false)
|
||||
|
||||
try {
|
||||
setForeground(getForegroundInfo())
|
||||
} catch (e: IllegalStateException) {
|
||||
logcat(LogPriority.ERROR, e) { "Not allowed to run on foreground service" }
|
||||
}
|
||||
setForegroundSafely()
|
||||
|
||||
return try {
|
||||
val restorer = BackupRestorer(context, notifier)
|
||||
|
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.coil
|
||||
|
||||
import androidx.core.net.toUri
|
||||
import coil.ImageLoader
|
||||
import coil.decode.DataSource
|
||||
import coil.decode.ImageSource
|
||||
@@ -10,6 +11,7 @@ import coil.fetch.SourceResult
|
||||
import coil.network.HttpException
|
||||
import coil.request.Options
|
||||
import coil.request.Parameters
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher.Companion.USE_CUSTOM_COVER
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
@@ -24,6 +26,7 @@ import okio.Path.Companion.toOkioPath
|
||||
import okio.Source
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import okio.source
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.model.MangaCover
|
||||
@@ -69,8 +72,9 @@ class MangaCoverFetcher(
|
||||
// diskCacheKey is thumbnail_url
|
||||
if (url == null) error("No cover specified")
|
||||
return when (getResourceType(url)) {
|
||||
Type.URL -> httpLoader()
|
||||
Type.File -> fileLoader(File(url.substringAfter("file://")))
|
||||
Type.URI -> fileUriLoader(url)
|
||||
Type.URL -> httpLoader()
|
||||
null -> error("Invalid image")
|
||||
}
|
||||
}
|
||||
@@ -83,6 +87,18 @@ class MangaCoverFetcher(
|
||||
)
|
||||
}
|
||||
|
||||
private fun fileUriLoader(uri: String): FetchResult {
|
||||
val source = UniFile.fromUri(options.context, uri.toUri())!!
|
||||
.openInputStream()
|
||||
.source()
|
||||
.buffer()
|
||||
return SourceResult(
|
||||
source = ImageSource(source = source, context = options.context),
|
||||
mimeType = "image/*",
|
||||
dataSource = DataSource.DISK,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun httpLoader(): FetchResult {
|
||||
// Only cache separately if it's a library item
|
||||
val libraryCoverCacheFile = if (isLibraryManga) {
|
||||
@@ -256,12 +272,15 @@ class MangaCoverFetcher(
|
||||
cover.isNullOrEmpty() -> null
|
||||
cover.startsWith("http", true) || cover.startsWith("Custom-", true) -> Type.URL
|
||||
cover.startsWith("/") || cover.startsWith("file://") -> Type.File
|
||||
cover.startsWith("content") -> Type.URI
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private enum class Type {
|
||||
File, URL
|
||||
File,
|
||||
URI,
|
||||
URL,
|
||||
}
|
||||
|
||||
class MangaFactory(
|
||||
|
@@ -94,7 +94,7 @@ class DownloadCache(
|
||||
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
|
||||
|
||||
private val diskCacheFile: File
|
||||
get() = File(context.cacheDir, "dl_index_cache")
|
||||
get() = File(context.cacheDir, "dl_index_cache_v2")
|
||||
|
||||
private val rootDownloadsDirLock = Mutex()
|
||||
private var rootDownloadsDir = RootDirectory(provider.downloadsDir)
|
||||
|
@@ -0,0 +1,117 @@
|
||||
package eu.kanade.tachiyomi.data.download
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import androidx.lifecycle.asFlow
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.util.system.isConnectedToWifi
|
||||
import eu.kanade.tachiyomi.util.system.isOnline
|
||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||
import eu.kanade.tachiyomi.util.system.setForegroundSafely
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import tachiyomi.domain.download.service.DownloadPreferences
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
/**
|
||||
* This worker is used to manage the downloader. The system can decide to stop the worker, in
|
||||
* which case the downloader is also stopped. It's also stopped while there's no network available.
|
||||
*/
|
||||
class DownloadJob(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) {
|
||||
|
||||
private val downloadManager: DownloadManager = Injekt.get()
|
||||
private val downloadPreferences: DownloadPreferences = Injekt.get()
|
||||
|
||||
override suspend fun getForegroundInfo(): ForegroundInfo {
|
||||
val notification = applicationContext.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
|
||||
setContentTitle(applicationContext.getString(R.string.download_notifier_downloader_title))
|
||||
setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
}.build()
|
||||
return ForegroundInfo(
|
||||
Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS,
|
||||
notification,
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
} else {
|
||||
0
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
var active = checkConnectivity() && downloadManager.downloaderStart()
|
||||
|
||||
if (!active) {
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
setForegroundSafely()
|
||||
|
||||
// Keep the worker running when needed
|
||||
while (active) {
|
||||
delay(100)
|
||||
active = !isStopped && downloadManager.isRunning && checkConnectivity()
|
||||
}
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
private fun checkConnectivity(): Boolean {
|
||||
return with(applicationContext) {
|
||||
if (isOnline()) {
|
||||
val noWifi = downloadPreferences.downloadOnlyOverWifi().get() && !isConnectedToWifi()
|
||||
if (noWifi) {
|
||||
downloadManager.downloaderStop(
|
||||
applicationContext.getString(R.string.download_notifier_text_only_wifi),
|
||||
)
|
||||
}
|
||||
!noWifi
|
||||
} else {
|
||||
downloadManager.downloaderStop(applicationContext.getString(R.string.download_notifier_no_network))
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "Downloader"
|
||||
|
||||
fun start(context: Context) {
|
||||
val request = OneTimeWorkRequestBuilder<DownloadJob>()
|
||||
.addTag(TAG)
|
||||
.build()
|
||||
WorkManager.getInstance(context)
|
||||
.enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, request)
|
||||
}
|
||||
|
||||
fun stop(context: Context) {
|
||||
WorkManager.getInstance(context)
|
||||
.cancelUniqueWork(TAG)
|
||||
}
|
||||
|
||||
fun isRunning(context: Context): Boolean {
|
||||
return WorkManager.getInstance(context)
|
||||
.getWorkInfosForUniqueWork(TAG)
|
||||
.get()
|
||||
.let { list -> list.count { it.state == WorkInfo.State.RUNNING } == 1 }
|
||||
}
|
||||
|
||||
fun isRunningFlow(context: Context): Flow<Boolean> {
|
||||
return WorkManager.getInstance(context)
|
||||
.getWorkInfosForUniqueWorkLiveData(TAG)
|
||||
.asFlow()
|
||||
.map { list -> list.count { it.state == WorkInfo.State.RUNNING } == 1 }
|
||||
}
|
||||
}
|
||||
}
|
@@ -46,6 +46,9 @@ class DownloadManager(
|
||||
*/
|
||||
private val downloader = Downloader(context, provider, cache)
|
||||
|
||||
val isRunning: Boolean
|
||||
get() = downloader.isRunning
|
||||
|
||||
/**
|
||||
* Queue to delay the deletion of a list of chapters until triggered.
|
||||
*/
|
||||
@@ -59,13 +62,19 @@ class DownloadManager(
|
||||
fun downloaderStop(reason: String? = null) = downloader.stop(reason)
|
||||
|
||||
val isDownloaderRunning
|
||||
get() = DownloadService.isRunning
|
||||
get() = DownloadJob.isRunningFlow(context)
|
||||
|
||||
/**
|
||||
* Tells the downloader to begin downloads.
|
||||
*/
|
||||
fun startDownloads() {
|
||||
DownloadService.start(context)
|
||||
if (downloader.isRunning) return
|
||||
|
||||
if (DownloadJob.isRunning(context)) {
|
||||
downloader.start()
|
||||
} else {
|
||||
DownloadJob.start(context)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,22 +103,16 @@ class DownloadManager(
|
||||
return queueState.value.find { it.chapter.id == chapterId }
|
||||
}
|
||||
|
||||
fun startDownloadNow(chapterId: Long?) {
|
||||
if (chapterId == null) return
|
||||
val download = getQueuedDownloadOrNull(chapterId)
|
||||
fun startDownloadNow(chapterId: Long) {
|
||||
val existingDownload = getQueuedDownloadOrNull(chapterId)
|
||||
// If not in queue try to start a new download
|
||||
val toAdd = download ?: runBlocking { Download.fromChapterId(chapterId) } ?: return
|
||||
val queue = queueState.value.toMutableList()
|
||||
download?.let { queue.remove(it) }
|
||||
queue.add(0, toAdd)
|
||||
reorderQueue(queue)
|
||||
if (!downloader.isRunning) {
|
||||
if (DownloadService.isRunning(context)) {
|
||||
downloader.start()
|
||||
} else {
|
||||
DownloadService.start(context)
|
||||
}
|
||||
val toAdd = existingDownload ?: runBlocking { Download.fromChapterId(chapterId) } ?: return
|
||||
queueState.value.toMutableList().apply {
|
||||
existingDownload?.let { remove(it) }
|
||||
add(0, toAdd)
|
||||
reorderQueue(this)
|
||||
}
|
||||
startDownloads()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,7 +146,7 @@ class DownloadManager(
|
||||
addAll(0, downloads)
|
||||
reorderQueue(this)
|
||||
}
|
||||
if (!DownloadService.isRunning(context)) DownloadService.start(context)
|
||||
if (!DownloadJob.isRunning(context)) startDownloads()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,151 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.download
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import androidx.core.content.ContextCompat
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||
import eu.kanade.tachiyomi.util.system.isConnectedToWifi
|
||||
import eu.kanade.tachiyomi.util.system.isOnline
|
||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import logcat.LogPriority
|
||||
import ru.beryukhov.reactivenetwork.ReactiveNetwork
|
||||
import tachiyomi.core.i18n.stringResource
|
||||
import tachiyomi.core.util.lang.withUIContext
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.download.service.DownloadPreferences
|
||||
import tachiyomi.i18n.MR
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* This service is used to manage the downloader. The system can decide to stop the service, in
|
||||
* which case the downloader is also stopped. It's also stopped while there's no network available.
|
||||
* While the downloader is running, a wake lock will be held.
|
||||
*/
|
||||
class DownloadService : Service() {
|
||||
|
||||
companion object {
|
||||
|
||||
private val _isRunning = MutableStateFlow(false)
|
||||
val isRunning = _isRunning.asStateFlow()
|
||||
|
||||
/**
|
||||
* Starts this service.
|
||||
*
|
||||
* @param context the application context.
|
||||
*/
|
||||
fun start(context: Context) {
|
||||
val intent = Intent(context, DownloadService::class.java)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops this service.
|
||||
*
|
||||
* @param context the application context.
|
||||
*/
|
||||
fun stop(context: Context) {
|
||||
context.stopService(Intent(context, DownloadService::class.java))
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the status of the service.
|
||||
*
|
||||
* @param context the application context.
|
||||
* @return true if the service is running, false otherwise.
|
||||
*/
|
||||
fun isRunning(context: Context): Boolean {
|
||||
return context.isServiceRunning(DownloadService::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
private val downloadManager: DownloadManager by injectLazy()
|
||||
private val downloadPreferences: DownloadPreferences by injectLazy()
|
||||
|
||||
/**
|
||||
* Wake lock to prevent the device to enter sleep mode.
|
||||
*/
|
||||
private lateinit var wakeLock: PowerManager.WakeLock
|
||||
|
||||
private lateinit var scope: CoroutineScope
|
||||
|
||||
override fun onCreate() {
|
||||
scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
startForeground(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS, getPlaceholderNotification())
|
||||
wakeLock = acquireWakeLock(javaClass.name)
|
||||
_isRunning.value = true
|
||||
listenNetworkChanges()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
scope.cancel()
|
||||
_isRunning.value = false
|
||||
downloadManager.downloaderStop()
|
||||
if (wakeLock.isHeld) {
|
||||
wakeLock.release()
|
||||
}
|
||||
}
|
||||
|
||||
// Not used
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
// Not used
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
return null
|
||||
}
|
||||
|
||||
private fun downloaderStop(string: StringResource) {
|
||||
downloadManager.downloaderStop(stringResource(string))
|
||||
}
|
||||
|
||||
private fun listenNetworkChanges() {
|
||||
ReactiveNetwork()
|
||||
.observeNetworkConnectivity(applicationContext)
|
||||
.onEach {
|
||||
withUIContext {
|
||||
if (isOnline()) {
|
||||
if (downloadPreferences.downloadOnlyOverWifi().get() && !isConnectedToWifi()) {
|
||||
downloaderStop(MR.strings.download_notifier_text_only_wifi)
|
||||
} else {
|
||||
val started = downloadManager.downloaderStart()
|
||||
if (!started) stopSelf()
|
||||
}
|
||||
} else {
|
||||
downloaderStop(MR.strings.download_notifier_no_network)
|
||||
}
|
||||
}
|
||||
}
|
||||
.catch { error ->
|
||||
withUIContext {
|
||||
logcat(LogPriority.ERROR, error)
|
||||
toast(MR.strings.download_queue_error)
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
private fun getPlaceholderNotification(): Notification {
|
||||
return notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
|
||||
setContentTitle(stringResource(MR.strings.download_notifier_downloader_title))
|
||||
}.build()
|
||||
}
|
||||
}
|
@@ -161,10 +161,7 @@ class Downloader(
|
||||
|
||||
isPaused = false
|
||||
|
||||
// Prevent recursion when DownloadService.onDestroy() calls downloader.stop()
|
||||
if (DownloadService.isRunning.value) {
|
||||
DownloadService.stop(context)
|
||||
}
|
||||
DownloadJob.stop(context)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -310,7 +307,7 @@ class Downloader(
|
||||
)
|
||||
}
|
||||
}
|
||||
DownloadService.start(context)
|
||||
DownloadJob.start(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,8 @@
|
||||
package eu.kanade.tachiyomi.data.library
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.CoroutineWorker
|
||||
@@ -28,6 +30,7 @@ import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
||||
import eu.kanade.tachiyomi.util.system.isConnectedToWifi
|
||||
import eu.kanade.tachiyomi.util.system.isRunning
|
||||
import eu.kanade.tachiyomi.util.system.setForegroundSafely
|
||||
import eu.kanade.tachiyomi.util.system.workManager
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.async
|
||||
@@ -106,11 +109,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
setForeground(getForegroundInfo())
|
||||
} catch (e: IllegalStateException) {
|
||||
logcat(LogPriority.ERROR, e) { "Not allowed to set foreground job" }
|
||||
}
|
||||
setForegroundSafely()
|
||||
|
||||
libraryPreferences.lastUpdatedTimestamp().set(Date().time)
|
||||
|
||||
@@ -140,6 +139,11 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
return ForegroundInfo(
|
||||
Notifications.ID_LIBRARY_PROGRESS,
|
||||
notifier.progressNotificationBuilder.build(),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
} else {
|
||||
0
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,8 @@
|
||||
package eu.kanade.tachiyomi.data.library
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.ForegroundInfo
|
||||
@@ -16,6 +18,7 @@ import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.source.UnmeteredSource
|
||||
import eu.kanade.tachiyomi.util.prepUpdateCover
|
||||
import eu.kanade.tachiyomi.util.system.isRunning
|
||||
import eu.kanade.tachiyomi.util.system.setForegroundSafely
|
||||
import eu.kanade.tachiyomi.util.system.workManager
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.async
|
||||
@@ -51,11 +54,7 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame
|
||||
private var mangaToUpdate: List<LibraryManga> = mutableListOf()
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
try {
|
||||
setForeground(getForegroundInfo())
|
||||
} catch (e: IllegalStateException) {
|
||||
logcat(LogPriority.ERROR, e) { "Not allowed to set foreground job" }
|
||||
}
|
||||
setForegroundSafely()
|
||||
|
||||
addMangaToQueue()
|
||||
|
||||
@@ -82,6 +81,11 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame
|
||||
return ForegroundInfo(
|
||||
Notifications.ID_LIBRARY_PROGRESS,
|
||||
notifier.progressNotificationBuilder.build(),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
} else {
|
||||
0
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
@@ -7,6 +7,7 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.core.net.toUri
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.data.backup.BackupRestoreJob
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||
@@ -15,7 +16,6 @@ import eu.kanade.tachiyomi.data.updater.AppUpdateDownloadJob
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
import eu.kanade.tachiyomi.util.system.cancelNotification
|
||||
import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat
|
||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||
@@ -36,7 +36,6 @@ import tachiyomi.i18n.MR
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||
|
||||
/**
|
||||
@@ -65,15 +64,13 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
ACTION_SHARE_IMAGE ->
|
||||
shareImage(
|
||||
context,
|
||||
intent.getStringExtra(EXTRA_FILE_LOCATION)!!,
|
||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1),
|
||||
intent.getStringExtra(EXTRA_URI)!!.toUri(),
|
||||
)
|
||||
// Delete image from path and dismiss notification
|
||||
ACTION_DELETE_IMAGE ->
|
||||
deleteImage(
|
||||
context,
|
||||
intent.getStringExtra(EXTRA_FILE_LOCATION)!!,
|
||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1),
|
||||
intent.getStringExtra(EXTRA_URI)!!.toUri(),
|
||||
)
|
||||
// Share backup file
|
||||
ACTION_SHARE_BACKUP ->
|
||||
@@ -81,7 +78,6 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
context,
|
||||
intent.getParcelableExtraCompat(EXTRA_URI)!!,
|
||||
"application/x-protobuf+gzip",
|
||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1),
|
||||
)
|
||||
ACTION_CANCEL_RESTORE -> cancelRestore(context)
|
||||
|
||||
@@ -140,12 +136,10 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
* Called to start share intent to share image
|
||||
*
|
||||
* @param context context of application
|
||||
* @param path path of file
|
||||
* @param notificationId id of notification
|
||||
* @param uri path of file
|
||||
*/
|
||||
private fun shareImage(context: Context, path: String, notificationId: Int) {
|
||||
dismissNotification(context, notificationId)
|
||||
context.startActivity(File(path).getUriCompat(context).toShareIntent(context))
|
||||
private fun shareImage(context: Context, uri: Uri) {
|
||||
context.startActivity(uri.toShareIntent(context))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,10 +147,8 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
*
|
||||
* @param context context of application
|
||||
* @param path path of file
|
||||
* @param notificationId id of notification
|
||||
*/
|
||||
private fun shareFile(context: Context, uri: Uri, fileMimeType: String, notificationId: Int) {
|
||||
dismissNotification(context, notificationId)
|
||||
private fun shareFile(context: Context, uri: Uri, fileMimeType: String) {
|
||||
context.startActivity(uri.toShareIntent(context, fileMimeType))
|
||||
}
|
||||
|
||||
@@ -183,17 +175,11 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
/**
|
||||
* Called to delete image
|
||||
*
|
||||
* @param path path of file
|
||||
* @param notificationId id of notification
|
||||
* @param uri path of file
|
||||
*/
|
||||
private fun deleteImage(context: Context, path: String, notificationId: Int) {
|
||||
dismissNotification(context, notificationId)
|
||||
|
||||
// Delete file
|
||||
val file = File(path)
|
||||
file.delete()
|
||||
|
||||
DiskUtil.scanMedia(context, file.toUri())
|
||||
private fun deleteImage(context: Context, uri: Uri) {
|
||||
UniFile.fromUri(context, uri)?.delete()
|
||||
DiskUtil.scanMedia(context, uri)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -423,18 +409,17 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [PendingIntent] that starts a service which cancels the notification and starts a share activity
|
||||
* Returns [PendingIntent] that starts a share activity
|
||||
*
|
||||
* @param context context of application
|
||||
* @param path location path of file
|
||||
* @param uri location path of file
|
||||
* @param notificationId id of notification
|
||||
* @return [PendingIntent]
|
||||
*/
|
||||
internal fun shareImagePendingBroadcast(context: Context, path: String, notificationId: Int): PendingIntent {
|
||||
internal fun shareImagePendingBroadcast(context: Context, uri: Uri): PendingIntent {
|
||||
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
||||
action = ACTION_SHARE_IMAGE
|
||||
putExtra(EXTRA_FILE_LOCATION, path)
|
||||
putExtra(EXTRA_NOTIFICATION_ID, notificationId)
|
||||
putExtra(EXTRA_URI, uri.toString())
|
||||
}
|
||||
return PendingIntent.getBroadcast(
|
||||
context,
|
||||
@@ -448,15 +433,13 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
* Returns [PendingIntent] that starts a service which removes an image from disk
|
||||
*
|
||||
* @param context context of application
|
||||
* @param path location path of file
|
||||
* @param notificationId id of notification
|
||||
* @param uri location path of file
|
||||
* @return [PendingIntent]
|
||||
*/
|
||||
internal fun deleteImagePendingBroadcast(context: Context, path: String, notificationId: Int): PendingIntent {
|
||||
internal fun deleteImagePendingBroadcast(context: Context, uri: Uri): PendingIntent {
|
||||
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
||||
action = ACTION_DELETE_IMAGE
|
||||
putExtra(EXTRA_FILE_LOCATION, path)
|
||||
putExtra(EXTRA_NOTIFICATION_ID, notificationId)
|
||||
putExtra(EXTRA_URI, uri.toString())
|
||||
}
|
||||
return PendingIntent.getBroadcast(
|
||||
context,
|
||||
@@ -639,14 +622,12 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
*
|
||||
* @param context context of application
|
||||
* @param uri uri of backup file
|
||||
* @param notificationId id of notification
|
||||
* @return [PendingIntent]
|
||||
*/
|
||||
internal fun shareBackupPendingBroadcast(context: Context, uri: Uri, notificationId: Int): PendingIntent {
|
||||
internal fun shareBackupPendingBroadcast(context: Context, uri: Uri): PendingIntent {
|
||||
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
||||
action = ACTION_SHARE_BACKUP
|
||||
putExtra(EXTRA_URI, uri)
|
||||
putExtra(EXTRA_NOTIFICATION_ID, notificationId)
|
||||
}
|
||||
return PendingIntent.getBroadcast(
|
||||
context,
|
||||
|
@@ -153,6 +153,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|month
|
||||
|day
|
||||
|}
|
||||
|averageScore
|
||||
|}
|
||||
|}
|
||||
|}
|
||||
@@ -309,6 +310,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
struct["status"]!!.jsonPrimitive.contentOrNull ?: "",
|
||||
parseDate(struct, "startDate"),
|
||||
struct["chapters"]!!.jsonPrimitive.intOrNull ?: 0,
|
||||
struct["averageScore"]?.jsonPrimitive?.intOrNull ?: -1,
|
||||
)
|
||||
}
|
||||
|
||||
|
@@ -19,6 +19,7 @@ data class ALManga(
|
||||
val publishing_status: String,
|
||||
val start_date_fuzzy: Long,
|
||||
val total_chapters: Int,
|
||||
val average_score: Int,
|
||||
) {
|
||||
|
||||
fun toTrack() = TrackSearch.create(TrackerManager.ANILIST).apply {
|
||||
@@ -27,6 +28,7 @@ data class ALManga(
|
||||
total_chapters = this@ALManga.total_chapters
|
||||
cover_url = image_url_lge
|
||||
summary = description?.htmlDecode() ?: ""
|
||||
score = average_score.toFloat()
|
||||
tracking_url = AnilistApi.mangaUrl(media_id)
|
||||
publishing_status = this@ALManga.publishing_status
|
||||
publishing_type = format
|
||||
|
@@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.network.parseAs
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.floatOrNull
|
||||
import kotlinx.serialization.json.int
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
@@ -108,11 +109,13 @@ class BangumiApi(
|
||||
} else {
|
||||
0
|
||||
}
|
||||
val rating = obj["rating"]?.jsonObject?.get("score")?.jsonPrimitive?.floatOrNull ?: -1f
|
||||
return TrackSearch.create(trackId).apply {
|
||||
media_id = obj["id"]!!.jsonPrimitive.long
|
||||
title = obj["name_cn"]!!.jsonPrimitive.content
|
||||
cover_url = coverUrl
|
||||
summary = obj["name"]!!.jsonPrimitive.content
|
||||
score = rating
|
||||
tracking_url = obj["url"]!!.jsonPrimitive.content
|
||||
total_chapters = totalChapters
|
||||
}
|
||||
|
@@ -279,7 +279,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
||||
private const val algoliaAppId = "AWQO5J657S"
|
||||
private const val algoliaFilter =
|
||||
"&facetFilters=%5B%22kind%3Amanga%22%5D&attributesToRetrieve=" +
|
||||
"%5B%22synopsis%22%2C%22canonicalTitle%22%2C%22chapterCount%22%2C%22" +
|
||||
"%5B%22synopsis%22%2C%22averageRating%22%2C%22canonicalTitle%22%2C%22chapterCount%22%2C%22" +
|
||||
"posterImage%22%2C%22startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D"
|
||||
|
||||
fun mangaUrl(remoteId: Long): String {
|
||||
|
@@ -28,6 +28,7 @@ class KitsuSearchManga(obj: JsonObject) {
|
||||
null
|
||||
}
|
||||
private val synopsis = obj["synopsis"]?.jsonPrimitive?.contentOrNull
|
||||
private val rating = obj["averageRating"]?.jsonPrimitive?.contentOrNull?.toFloatOrNull()
|
||||
private var startDate = obj["startDate"]?.jsonPrimitive?.contentOrNull?.let {
|
||||
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||
outputDf.format(Date(it.toLong() * 1000))
|
||||
@@ -42,6 +43,7 @@ class KitsuSearchManga(obj: JsonObject) {
|
||||
cover_url = original ?: ""
|
||||
summary = synopsis ?: ""
|
||||
tracking_url = KitsuApi.mangaUrl(media_id)
|
||||
score = rating ?: -1f
|
||||
publishing_status = if (endDate == null) {
|
||||
"Publishing"
|
||||
} else {
|
||||
|
@@ -20,7 +20,7 @@ class TrackSearch : Track {
|
||||
|
||||
override var total_chapters: Int = 0
|
||||
|
||||
override var score: Float = 0f
|
||||
override var score: Float = -1f
|
||||
|
||||
override var status: Int = 0
|
||||
|
||||
|
@@ -16,6 +16,7 @@ import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.boolean
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.float
|
||||
import kotlinx.serialization.json.floatOrNull
|
||||
import kotlinx.serialization.json.int
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
@@ -103,7 +104,7 @@ class MyAnimeListApi(
|
||||
.appendPath(id.toString())
|
||||
.appendQueryParameter(
|
||||
"fields",
|
||||
"id,title,synopsis,num_chapters,main_picture,status,media_type,start_date",
|
||||
"id,title,synopsis,num_chapters,mean,main_picture,status,media_type,start_date",
|
||||
)
|
||||
.build()
|
||||
with(json) {
|
||||
@@ -117,6 +118,7 @@ class MyAnimeListApi(
|
||||
title = obj["title"]!!.jsonPrimitive.content
|
||||
summary = obj["synopsis"]?.jsonPrimitive?.content ?: ""
|
||||
total_chapters = obj["num_chapters"]!!.jsonPrimitive.int
|
||||
score = obj["mean"]?.jsonPrimitive?.floatOrNull ?: -1f
|
||||
cover_url =
|
||||
obj["main_picture"]?.jsonObject?.get("large")?.jsonPrimitive?.content
|
||||
?: ""
|
||||
|
@@ -107,6 +107,7 @@ class ShikimoriApi(
|
||||
total_chapters = obj["chapters"]!!.jsonPrimitive.int
|
||||
cover_url = baseUrl + obj["image"]!!.jsonObject["preview"]!!.jsonPrimitive.content
|
||||
summary = ""
|
||||
score = obj["score"]!!.jsonPrimitive.float
|
||||
tracking_url = baseUrl + obj["url"]!!.jsonPrimitive.content
|
||||
publishing_status = obj["status"]!!.jsonPrimitive.content
|
||||
publishing_type = obj["kind"]!!.jsonPrimitive.content
|
||||
|
@@ -17,13 +17,12 @@ import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.network.newCachelessCallWithProgress
|
||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||
import eu.kanade.tachiyomi.util.system.setForegroundSafely
|
||||
import eu.kanade.tachiyomi.util.system.workManager
|
||||
import logcat.LogPriority
|
||||
import okhttp3.internal.http2.ErrorCode
|
||||
import okhttp3.internal.http2.StreamResetException
|
||||
import tachiyomi.core.i18n.stringResource
|
||||
import tachiyomi.core.util.lang.withIOContext
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.i18n.MR
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
@@ -43,11 +42,7 @@ class AppUpdateDownloadJob(private val context: Context, workerParams: WorkerPar
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
try {
|
||||
setForeground(getForegroundInfo())
|
||||
} catch (e: IllegalStateException) {
|
||||
logcat(LogPriority.ERROR, e) { "Not allowed to run on foreground service" }
|
||||
}
|
||||
setForegroundSafely()
|
||||
|
||||
withIOContext {
|
||||
downloadApk(title, url)
|
||||
|
@@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.core.security.SecurityPreferences
|
||||
import eu.kanade.tachiyomi.ui.security.UnlockActivity
|
||||
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil
|
||||
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.isAuthenticationSupported
|
||||
import eu.kanade.tachiyomi.util.system.overridePendingTransitionCompat
|
||||
import eu.kanade.tachiyomi.util.view.setSecureScreen
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@@ -107,7 +106,7 @@ class SecureActivityDelegateImpl : SecureActivityDelegate, DefaultLifecycleObser
|
||||
if (activity.isAuthenticationSupported()) {
|
||||
if (!SecureActivityDelegate.requireUnlock) return
|
||||
activity.startActivity(Intent(activity, UnlockActivity::class.java))
|
||||
activity.overridePendingTransitionCompat(0, 0)
|
||||
activity.overridePendingTransition(0, 0)
|
||||
} else {
|
||||
securityPreferences.useAuthenticator().set(false)
|
||||
}
|
||||
|
@@ -6,7 +6,6 @@ import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.download.DownloadCache
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.i18n.MR
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
data class MigrationFlag(
|
||||
|
@@ -11,12 +11,14 @@ import eu.kanade.tachiyomi.source.model.Page
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import uy.kohesive.injekt.Injekt
|
||||
@@ -137,8 +139,8 @@ class DownloadQueueScreenModel(
|
||||
adapter = null
|
||||
}
|
||||
|
||||
val isDownloaderRunning
|
||||
get() = downloadManager.isDownloaderRunning
|
||||
val isDownloaderRunning = downloadManager.isDownloaderRunning
|
||||
.stateIn(screenModelScope, SharingStarted.WhileSubscribed(5000), false)
|
||||
|
||||
fun getDownloadStatusFlow() = downloadManager.statusFlow()
|
||||
fun getDownloadProgressFlow() = downloadManager.progressFlow()
|
||||
|
@@ -34,17 +34,16 @@ import androidx.core.transition.doOnEnd
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import com.google.android.material.elevation.SurfaceColors
|
||||
import com.google.android.material.transition.platform.MaterialContainerTransform
|
||||
import dev.chrisbanes.insetter.applyInsetter
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.presentation.reader.BrightnessOverlay
|
||||
import eu.kanade.presentation.reader.DisplayRefreshHost
|
||||
import eu.kanade.presentation.reader.OrientationSelectDialog
|
||||
import eu.kanade.presentation.reader.PageIndicatorText
|
||||
import eu.kanade.presentation.reader.ReaderContentOverlay
|
||||
import eu.kanade.presentation.reader.ReaderPageActionsDialog
|
||||
import eu.kanade.presentation.reader.ReadingModeSelectDialog
|
||||
import eu.kanade.presentation.reader.appbars.ReaderAppBars
|
||||
@@ -70,7 +69,6 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
|
||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
||||
import eu.kanade.tachiyomi.util.system.hasDisplayCutout
|
||||
import eu.kanade.tachiyomi.util.system.isNightMode
|
||||
import eu.kanade.tachiyomi.util.system.overridePendingTransitionCompat
|
||||
import eu.kanade.tachiyomi.util.system.toShareIntent
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.util.view.setComposeContent
|
||||
@@ -90,7 +88,6 @@ import tachiyomi.core.util.lang.launchIO
|
||||
import tachiyomi.core.util.lang.launchNonCancellable
|
||||
import tachiyomi.core.util.lang.withUIContext
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
import uy.kohesive.injekt.Injekt
|
||||
@@ -139,7 +136,7 @@ class ReaderActivity : BaseActivity() {
|
||||
*/
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
registerSecureActivity(this)
|
||||
overridePendingTransitionCompat(R.anim.shared_axis_x_push_enter, R.anim.shared_axis_x_push_exit)
|
||||
overridePendingTransition(R.anim.shared_axis_x_push_enter, R.anim.shared_axis_x_push_exit)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -185,7 +182,7 @@ class ReaderActivity : BaseActivity() {
|
||||
.map { it.manga }
|
||||
.distinctUntilChanged()
|
||||
.filterNotNull()
|
||||
.onEach(::setManga)
|
||||
.onEach { updateViewer() }
|
||||
.launchIn(lifecycleScope)
|
||||
|
||||
viewModel.state
|
||||
@@ -270,7 +267,7 @@ class ReaderActivity : BaseActivity() {
|
||||
override fun finish() {
|
||||
viewModel.onActivityFinish()
|
||||
super.finish()
|
||||
overridePendingTransitionCompat(R.anim.shared_axis_x_pop_enter, R.anim.shared_axis_x_pop_exit)
|
||||
overridePendingTransition(R.anim.shared_axis_x_pop_enter, R.anim.shared_axis_x_pop_exit)
|
||||
}
|
||||
|
||||
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
|
||||
@@ -332,11 +329,24 @@ class ReaderActivity : BaseActivity() {
|
||||
val isFullscreen by readerPreferences.fullscreen().collectAsState()
|
||||
val flashOnPageChange by readerPreferences.flashOnPageChange().collectAsState()
|
||||
|
||||
val colorOverlayEnabled by readerPreferences.colorFilter().collectAsState()
|
||||
val colorOverlay by readerPreferences.colorFilterValue().collectAsState()
|
||||
val colorOverlayMode by readerPreferences.colorFilterMode().collectAsState()
|
||||
val colorOverlayBlendMode = remember(colorOverlayMode) {
|
||||
ReaderPreferences.ColorFilterMode.getOrNull(colorOverlayMode)?.second
|
||||
}
|
||||
|
||||
val cropBorderPaged by readerPreferences.cropBorders().collectAsState()
|
||||
val cropBorderWebtoon by readerPreferences.cropBordersWebtoon().collectAsState()
|
||||
val isPagerType = ReadingMode.isPagerType(viewModel.getMangaReadingMode())
|
||||
val cropEnabled = if (isPagerType) cropBorderPaged else cropBorderWebtoon
|
||||
|
||||
ReaderContentOverlay(
|
||||
brightness = state.brightnessOverlayValue,
|
||||
color = colorOverlay.takeIf { colorOverlayEnabled },
|
||||
colorBlendMode = colorOverlayBlendMode,
|
||||
)
|
||||
|
||||
ReaderAppBars(
|
||||
visible = state.menuVisible,
|
||||
fullscreen = isFullscreen,
|
||||
@@ -379,10 +389,6 @@ class ReaderActivity : BaseActivity() {
|
||||
onClickSettings = viewModel::openSettingsDialog,
|
||||
)
|
||||
|
||||
BrightnessOverlay(
|
||||
value = state.brightnessOverlayValue,
|
||||
)
|
||||
|
||||
if (flashOnPageChange) {
|
||||
DisplayRefreshHost(
|
||||
hostState = displayRefreshHost,
|
||||
@@ -479,10 +485,9 @@ class ReaderActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the presenter when a manga is ready. Used to instantiate the appropriate viewer
|
||||
* and the toolbar title.
|
||||
* Called from the presenter when a manga is ready. Used to instantiate the appropriate viewer.
|
||||
*/
|
||||
private fun setManga(manga: Manga) {
|
||||
private fun updateViewer() {
|
||||
val prevViewer = viewModel.state.value.viewer
|
||||
val newViewer = ReadingMode.toViewer(viewModel.getMangaReadingMode(), this)
|
||||
|
||||
@@ -806,14 +811,6 @@ class ReaderActivity : BaseActivity() {
|
||||
.onEach(::setCustomBrightness)
|
||||
.launchIn(lifecycleScope)
|
||||
|
||||
readerPreferences.colorFilter().changes()
|
||||
.onEach(::setColorFilter)
|
||||
.launchIn(lifecycleScope)
|
||||
|
||||
readerPreferences.colorFilterMode().changes()
|
||||
.onEach { setColorFilter(readerPreferences.colorFilter().get()) }
|
||||
.launchIn(lifecycleScope)
|
||||
|
||||
merge(readerPreferences.grayscale().changes(), readerPreferences.invertedColors().changes())
|
||||
.onEach { setLayerPaint(readerPreferences.grayscale().get(), readerPreferences.invertedColors().get()) }
|
||||
.launchIn(lifecycleScope)
|
||||
@@ -885,20 +882,6 @@ class ReaderActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the color filter overlay according to [enabled].
|
||||
*/
|
||||
private fun setColorFilter(enabled: Boolean) {
|
||||
if (enabled) {
|
||||
readerPreferences.colorFilterValue().changes()
|
||||
.sample(100)
|
||||
.onEach(::setColorFilterValue)
|
||||
.launchIn(lifecycleScope)
|
||||
} else {
|
||||
binding.colorOverlay.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the brightness of the screen. Range is [-75, 100].
|
||||
* From -75 to -1 a semi-transparent black view is overlaid with the minimum brightness.
|
||||
@@ -920,15 +903,6 @@ class ReaderActivity : BaseActivity() {
|
||||
|
||||
viewModel.setBrightnessOverlayValue(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the color filter [value].
|
||||
*/
|
||||
private fun setColorFilterValue(value: Int) {
|
||||
binding.colorOverlay.isVisible = true
|
||||
binding.colorOverlay.setFilterColor(value, readerPreferences.colorFilterMode().get())
|
||||
}
|
||||
|
||||
private fun setLayerPaint(grayscale: Boolean, invertedColors: Boolean) {
|
||||
val paint = if (grayscale || invertedColors) getCombinedPaint(grayscale, invertedColors) else null
|
||||
binding.viewerContainer.setLayerType(LAYER_TYPE_HARDWARE, paint)
|
||||
|
@@ -1,36 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.reader
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PorterDuff
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.core.graphics.toXfermode
|
||||
|
||||
class ReaderColorFilterView(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
) : View(context, attrs) {
|
||||
|
||||
private val colorFilterPaint: Paint = Paint()
|
||||
|
||||
fun setFilterColor(color: Int, filterMode: Int) {
|
||||
colorFilterPaint.color = color
|
||||
colorFilterPaint.xfermode = when (filterMode) {
|
||||
1 -> PorterDuff.Mode.MULTIPLY
|
||||
2 -> PorterDuff.Mode.SCREEN
|
||||
3 -> PorterDuff.Mode.OVERLAY
|
||||
4 -> PorterDuff.Mode.LIGHTEN
|
||||
5 -> PorterDuff.Mode.DARKEN
|
||||
else -> PorterDuff.Mode.SRC_OVER
|
||||
}.toXfermode()
|
||||
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
canvas.drawPaint(colorFilterPaint)
|
||||
}
|
||||
}
|
@@ -81,13 +81,13 @@ class SaveImageNotifier(private val context: Context) {
|
||||
addAction(
|
||||
R.drawable.ic_share_24dp,
|
||||
context.stringResource(MR.strings.action_share),
|
||||
NotificationReceiver.shareImagePendingBroadcast(context, uri.path!!, notificationId),
|
||||
NotificationReceiver.shareImagePendingBroadcast(context, uri),
|
||||
)
|
||||
// Delete action
|
||||
addAction(
|
||||
R.drawable.ic_delete_24dp,
|
||||
context.stringResource(MR.strings.action_delete),
|
||||
NotificationReceiver.deleteImagePendingBroadcast(context, uri.path!!, notificationId),
|
||||
NotificationReceiver.deleteImagePendingBroadcast(context, uri),
|
||||
)
|
||||
|
||||
updateNotification()
|
||||
|
@@ -1,57 +1,62 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.setting
|
||||
|
||||
import android.content.pm.ActivityInfo
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ScreenLockLandscape
|
||||
import androidx.compose.material.icons.filled.ScreenLockPortrait
|
||||
import androidx.compose.material.icons.filled.ScreenRotation
|
||||
import androidx.compose.material.icons.filled.StayCurrentLandscape
|
||||
import androidx.compose.material.icons.filled.StayCurrentPortrait
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.R
|
||||
import tachiyomi.i18n.MR
|
||||
|
||||
enum class ReaderOrientation(
|
||||
val flag: Int,
|
||||
val stringRes: StringResource,
|
||||
@DrawableRes val iconRes: Int,
|
||||
val icon: ImageVector,
|
||||
val flagValue: Int,
|
||||
) {
|
||||
DEFAULT(
|
||||
ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED,
|
||||
MR.strings.label_default,
|
||||
R.drawable.ic_screen_rotation_24dp,
|
||||
Icons.Default.ScreenRotation,
|
||||
0x00000000,
|
||||
),
|
||||
FREE(
|
||||
ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED,
|
||||
MR.strings.rotation_free,
|
||||
R.drawable.ic_screen_rotation_24dp,
|
||||
Icons.Default.ScreenRotation,
|
||||
0x00000008,
|
||||
),
|
||||
PORTRAIT(
|
||||
ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT,
|
||||
MR.strings.rotation_portrait,
|
||||
R.drawable.ic_stay_current_portrait_24dp,
|
||||
Icons.Default.StayCurrentPortrait,
|
||||
0x00000010,
|
||||
),
|
||||
LANDSCAPE(
|
||||
ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE,
|
||||
MR.strings.rotation_landscape,
|
||||
R.drawable.ic_stay_current_landscape_24dp,
|
||||
Icons.Default.StayCurrentLandscape,
|
||||
0x00000018,
|
||||
),
|
||||
LOCKED_PORTRAIT(
|
||||
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT,
|
||||
MR.strings.rotation_force_portrait,
|
||||
R.drawable.ic_screen_lock_portrait_24dp,
|
||||
Icons.Default.ScreenLockPortrait,
|
||||
0x00000020,
|
||||
),
|
||||
LOCKED_LANDSCAPE(
|
||||
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE,
|
||||
MR.strings.rotation_force_landscape,
|
||||
R.drawable.ic_screen_lock_landscape_24dp,
|
||||
Icons.Default.ScreenLockLandscape,
|
||||
0x00000028,
|
||||
),
|
||||
REVERSE_PORTRAIT(
|
||||
ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT,
|
||||
MR.strings.rotation_reverse_portrait,
|
||||
R.drawable.ic_stay_current_portrait_24dp,
|
||||
Icons.Default.StayCurrentPortrait,
|
||||
0x00000030,
|
||||
),
|
||||
;
|
||||
|
@@ -1,5 +1,7 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.setting
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
import tachiyomi.core.preference.getEnum
|
||||
@@ -178,5 +180,24 @@ class ReaderPreferences(
|
||||
MR.strings.zoom_start_right,
|
||||
MR.strings.zoom_start_center,
|
||||
)
|
||||
|
||||
val ColorFilterMode = buildList {
|
||||
addAll(
|
||||
listOf(
|
||||
MR.strings.label_default to BlendMode.SrcOver,
|
||||
MR.strings.filter_mode_multiply to BlendMode.Modulate,
|
||||
MR.strings.filter_mode_screen to BlendMode.Screen,
|
||||
),
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
addAll(
|
||||
listOf(
|
||||
MR.strings.filter_mode_overlay to BlendMode.Overlay,
|
||||
MR.strings.filter_mode_lighten to BlendMode.Lighten,
|
||||
MR.strings.filter_mode_darken to BlendMode.Darken,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||
import eu.kanade.tachiyomi.util.system.overridePendingTransitionCompat
|
||||
import eu.kanade.tachiyomi.util.system.toShareIntent
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.util.view.setComposeContent
|
||||
@@ -36,7 +35,7 @@ class WebViewActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
overridePendingTransitionCompat(R.anim.shared_axis_x_push_enter, R.anim.shared_axis_x_push_exit)
|
||||
overridePendingTransition(R.anim.shared_axis_x_push_enter, R.anim.shared_axis_x_push_exit)
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (!WebViewUtil.supportsWebView(this)) {
|
||||
@@ -78,7 +77,7 @@ class WebViewActivity : BaseActivity() {
|
||||
|
||||
override fun finish() {
|
||||
super.finish()
|
||||
overridePendingTransitionCompat(R.anim.shared_axis_x_pop_enter, R.anim.shared_axis_x_pop_exit)
|
||||
overridePendingTransition(R.anim.shared_axis_x_pop_enter, R.anim.shared_axis_x_pop_exit)
|
||||
}
|
||||
|
||||
private fun shareWebpage(url: String) {
|
||||
|
@@ -1,14 +0,0 @@
|
||||
package eu.kanade.tachiyomi.util.system
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.annotation.AnimRes
|
||||
|
||||
fun Activity.overridePendingTransitionCompat(@AnimRes enterAnim: Int, @AnimRes exitAnim: Int) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
overrideActivityTransition(Activity.OVERRIDE_TRANSITION_OPEN, enterAnim, exitAnim)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
overridePendingTransition(enterAnim, exitAnim)
|
||||
}
|
||||
}
|
@@ -1,18 +1,15 @@
|
||||
package eu.kanade.tachiyomi.util.system
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
import androidx.core.content.PermissionChecker
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.net.toUri
|
||||
import com.hippo.unifile.UniFile
|
||||
@@ -55,39 +52,9 @@ fun Context.copyToClipboard(label: String, content: String) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the give permission is granted.
|
||||
*
|
||||
* @param permission the permission to check.
|
||||
* @return true if it has permissions.
|
||||
*/
|
||||
fun Context.hasPermission(
|
||||
permission: String,
|
||||
) = PermissionChecker.checkSelfPermission(this, permission) == PermissionChecker.PERMISSION_GRANTED
|
||||
|
||||
val Context.powerManager: PowerManager
|
||||
get() = getSystemService()!!
|
||||
|
||||
/**
|
||||
* Convenience method to acquire a partial wake lock.
|
||||
*/
|
||||
fun Context.acquireWakeLock(tag: String): PowerManager.WakeLock {
|
||||
val wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag:WakeLock")
|
||||
wakeLock.acquire()
|
||||
return wakeLock
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given service class is running.
|
||||
*/
|
||||
fun Context.isServiceRunning(serviceClass: Class<*>): Boolean {
|
||||
val className = serviceClass.name
|
||||
val manager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
||||
@Suppress("DEPRECATION")
|
||||
return manager.getRunningServices(Integer.MAX_VALUE)
|
||||
.any { className == it.service.className }
|
||||
}
|
||||
|
||||
fun Context.openInBrowser(url: String, forceDefaultBrowser: Boolean = false) {
|
||||
this.openInBrowser(url.toUri(), forceDefaultBrowser)
|
||||
}
|
||||
@@ -200,11 +167,3 @@ fun Context.isInstalledFromFDroid(): Boolean {
|
||||
// F-Droid builds typically disable the updater
|
||||
(!BuildConfig.INCLUDE_UPDATER && !isDevFlavor)
|
||||
}
|
||||
|
||||
fun Context.getApplicationIcon(pkgName: String): Drawable? {
|
||||
return try {
|
||||
packageManager.getApplicationIcon(pkgName)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
@@ -1,8 +1,12 @@
|
||||
package eu.kanade.tachiyomi.util.system
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import kotlinx.coroutines.delay
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.system.logcat
|
||||
|
||||
val Context.workManager: WorkManager
|
||||
get() = WorkManager.getInstance(this)
|
||||
@@ -11,3 +15,21 @@ fun WorkManager.isRunning(tag: String): Boolean {
|
||||
val list = this.getWorkInfosByTag(tag).get()
|
||||
return list.any { it.state == WorkInfo.State.RUNNING }
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes this worker run in the context of a foreground service.
|
||||
*
|
||||
* Note that this function is a no-op if the process is subject to foreground
|
||||
* service restrictions.
|
||||
*
|
||||
* Moving to foreground service context requires the worker to run a bit longer,
|
||||
* allowing Service.startForeground() to be called and avoiding system crash.
|
||||
*/
|
||||
suspend fun CoroutineWorker.setForegroundSafely() {
|
||||
try {
|
||||
setForeground(getForegroundInfo())
|
||||
delay(500)
|
||||
} catch (e: IllegalStateException) {
|
||||
logcat(LogPriority.ERROR, e) { "Not allowed to set foreground job" }
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 34% of 12% = ~4% -->
|
||||
<item android:alpha="0.34" android:color="?attr/colorControlHighlight" />
|
||||
</selector>
|
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="?attr/colorPrimary" android:state_enabled="true"/>
|
||||
<item android:alpha="@dimen/material_emphasis_disabled" android:color="?attr/colorOnSurface"/>
|
||||
</selector>
|
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:alpha="0.24" android:color="?attr/colorPrimary" android:state_enabled="true"/>
|
||||
<item android:alpha="@dimen/material_emphasis_disabled" android:color="?attr/colorOnSurface"/>
|
||||
</selector>
|
@@ -1,7 +0,0 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<size
|
||||
android:width="24dp"
|
||||
android:height="24dp" />
|
||||
<solid android:color="@android:color/transparent" />
|
||||
</shape>
|
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M12,2C6.5,2 2,6.5 2,12s4.5,10 10,10s10,-4.5 10,-10S17.5,2 12,2zM17,18H7v-2h10V18zM10.3,14L7,10.7l1.4,-1.4l1.9,1.9l5.3,-5.3L17,7.3L10.3,14z" />
|
||||
</vector>
|
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M21,5L3,5c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,7c0,-1.1 -0.9,-2 -2,-2zM19,17L5,17L5,7h14v10zM10,16h4c0.55,0 1,-0.45 1,-1v-3c0,-0.55 -0.45,-1 -1,-1v-1c0,-1.11 -0.9,-2 -2,-2 -1.11,0 -2,0.9 -2,2v1c-0.55,0 -1,0.45 -1,1v3c0,0.55 0.45,1 1,1zM10.8,10c0,-0.66 0.54,-1.2 1.2,-1.2 0.66,0 1.2,0.54 1.2,1.2v1h-2.4v-1z" />
|
||||
</vector>
|
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M10,16h4c0.55,0 1,-0.45 1,-1v-3c0,-0.55 -0.45,-1 -1,-1v-1c0,-1.11 -0.9,-2 -2,-2 -1.11,0 -2,0.9 -2,2v1c-0.55,0 -1,0.45 -1,1v3c0,0.55 0.45,1 1,1zM10.8,10c0,-0.66 0.54,-1.2 1.2,-1.2 0.66,0 1.2,0.54 1.2,1.2v1h-2.4v-1zM17,1L7,1c-1.1,0 -2,0.9 -2,2v18c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2L19,3c0,-1.1 -0.9,-2 -2,-2zM17,19L7,19L7,5h10v14z" />
|
||||
</vector>
|
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M16.48,2.52c3.27,1.55 5.61,4.72 5.97,8.48h1.5C23.44,4.84 18.29,0 12,0l-0.66,0.03 3.81,3.81 1.33,-1.32zM10.23,1.75c-0.59,-0.59 -1.54,-0.59 -2.12,0L1.75,8.11c-0.59,0.59 -0.59,1.54 0,2.12l12.02,12.02c0.59,0.59 1.54,0.59 2.12,0l6.36,-6.36c0.59,-0.59 0.59,-1.54 0,-2.12L10.23,1.75zM14.83,21.19L2.81,9.17l6.36,-6.36 12.02,12.02 -6.36,6.36zM7.52,21.48C4.25,19.94 1.91,16.76 1.55,13L0.05,13C0.56,19.16 5.71,24 12,24l0.66,-0.03 -3.81,-3.81 -1.33,1.32z" />
|
||||
</vector>
|
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M1.01,7L1,17c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2V7c0,-1.1 -0.9,-2 -2,-2H3c-1.1,0 -1.99,0.9 -1.99,2zM19,7v10H5V7h14z" />
|
||||
</vector>
|
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M17,1.01L7,1c-1.1,0 -1.99,0.9 -1.99,2v18c0,1.1 0.89,2 1.99,2h10c1.1,0 2,-0.9 2,-2V3c0,-1.1 -0.9,-1.99 -2,-1.99zM17,19H7V5h10v14z" />
|
||||
</vector>
|
@@ -1,7 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path android:fillColor="#FFF" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM11,19.93c-3.95,-0.49 -7,-3.85 -7,-7.93 0,-0.62 0.08,-1.21 0.21,-1.79L9,15v1c0,1.1 0.9,2 2,2v1.93zM17.9,17.39c-0.26,-0.81 -1,-1.39 -1.9,-1.39h-1v-3c0,-0.55 -0.45,-1 -1,-1L8,12v-2h2c0.55,0 1,-0.45 1,-1L11,7h2c1.1,0 2,-0.9 2,-2v-0.41c2.93,1.19 5,4.06 5,7.41 0,2.08 -0.8,3.97 -2.1,5.39z"/>
|
||||
</vector>
|
@@ -1,16 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@android:color/transparent"/>
|
||||
</shape>
|
||||
</item>
|
||||
<item
|
||||
android:gravity="bottom">
|
||||
<shape>
|
||||
<size android:height="1dp" />
|
||||
<solid android:color="?attr/colorSurfaceVariant" />
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
@@ -21,12 +21,6 @@
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<eu.kanade.tachiyomi.ui.reader.ReaderColorFilterView
|
||||
android:id="@+id/color_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
|
||||
<eu.kanade.tachiyomi.ui.reader.ReaderNavigationOverlayView
|
||||
android:id="@+id/navigation_overlay"
|
||||
android:layout_width="match_parent"
|
||||
|
@@ -1,3 +0,0 @@
|
||||
<resources>
|
||||
<dimen name="screen_edge_margin">24dp</dimen>
|
||||
</resources>
|
@@ -1,6 +1,4 @@
|
||||
<resources>
|
||||
<dimen name="screen_edge_margin">16dp</dimen>
|
||||
|
||||
<dimen name="appwidget_background_radius">16dp</dimen>
|
||||
<dimen name="appwidget_inner_radius">12dp</dimen>
|
||||
</resources>
|
||||
|
Reference in New Issue
Block a user