Merge branch 'master' into sync-part-final

This commit is contained in:
KaiserBh
2023-12-04 20:54:36 +11:00
committed by GitHub
104 changed files with 556 additions and 609 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()}\"")

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" />
@@ -38,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">

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

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

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

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

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

@@ -16,11 +16,10 @@ 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 logcat.LogPriority
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.download.service.DownloadPreferences
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@@ -51,21 +50,18 @@ class DownloadJob(context: Context, workerParams: WorkerParameters) : CoroutineW
}
override suspend fun doWork(): Result {
try {
setForeground(getForegroundInfo())
} catch (e: IllegalStateException) {
logcat(LogPriority.ERROR, e) { "Not allowed to set foreground job" }
var active = checkConnectivity() && downloadManager.downloaderStart()
if (!active) {
return Result.failure()
}
var networkCheck = checkConnectivity()
var active = networkCheck
downloadManager.downloaderStart()
setForegroundSafely()
// Keep the worker running when needed
while (active) {
delay(100)
networkCheck = checkConnectivity()
active = !isStopped && networkCheck && downloadManager.isRunning
active = !isStopped && downloadManager.isRunning && checkConnectivity()
}
return Result.success()

View File

@@ -68,7 +68,13 @@ class DownloadManager(
* Tells the downloader to begin downloads.
*/
fun startDownloads() {
DownloadJob.start(context)
if (downloader.isRunning) return
if (DownloadJob.isRunning(context)) {
downloader.start()
} else {
DownloadJob.start(context)
}
}
/**
@@ -97,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 (DownloadJob.isRunning(context)) {
downloader.start()
} else {
DownloadJob.start(context)
}
val toAdd = existingDownload ?: runBlocking { Download.fromChapterId(chapterId) } ?: return
queueState.value.toMutableList().apply {
existingDownload?.let { remove(it) }
add(0, toAdd)
reorderQueue(this)
}
startDownloads()
}
/**
@@ -146,7 +146,7 @@ class DownloadManager(
addAll(0, downloads)
reorderQueue(this)
}
if (!DownloadJob.isRunning(context)) DownloadJob.start(context)
if (!DownloadJob.isRunning(context)) startDownloads()
}
/**

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

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

@@ -10,7 +10,6 @@ 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
@@ -53,16 +52,6 @@ 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()!!

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

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