chore: merge upstream changes.

Signed-off-by: KaiserBh <kaiserbh@proton.me>
This commit is contained in:
KaiserBh
2023-12-05 23:53:20 +11:00
141 changed files with 745 additions and 932 deletions

View File

@@ -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)

View File

@@ -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"

View File

@@ -190,6 +190,7 @@ private fun ExtensionDetails(
key = { it.source.id },
) { source ->
SourceSwitchPreference(
modifier = Modifier.animateItemPlacement(),
source = source,
onClickSourcePreferences = onClickSourcePreferences,
onClickSource = onClickSource,

View File

@@ -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) },

View File

@@ -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) {

View File

@@ -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) },

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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) },

View File

@@ -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 {

View File

@@ -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

View File

@@ -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()
}
}

View File

@@ -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)
}
}

View File

@@ -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),
)
}

View File

@@ -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,
)
}
}
}

View File

@@ -49,7 +49,7 @@ fun BottomReaderBar(
IconButton(onClick = onClickOrientation) {
Icon(
painter = painterResource(orientation.iconRes),
imageVector = orientation.icon,
contentDescription = stringResource(MR.strings.rotation_type),
)
}

View File

@@ -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)) },
)
}
}

View File

@@ -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()) {

View File

@@ -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

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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),

View File

@@ -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)

View File

@@ -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)

View File

@@ -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(

View File

@@ -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)

View File

@@ -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 }
}
}
}

View File

@@ -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()
}
/**

View File

@@ -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()
}
}

View File

@@ -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)
}
}
}

View File

@@ -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
},
)
}

View File

@@ -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
},
)
}

View File

@@ -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,

View File

@@ -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,
)
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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

View File

@@ -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
?: ""

View File

@@ -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

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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(

View File

@@ -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()

View File

@@ -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)

View File

@@ -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)
}
}

View File

@@ -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()

View File

@@ -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,
),
;

View File

@@ -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,
),
)
}
}
}
}

View File

@@ -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) {

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -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" }
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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"

View File

@@ -1,3 +0,0 @@
<resources>
<dimen name="screen_edge_margin">24dp</dimen>
</resources>

View File

@@ -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>