mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-26 19:17:51 +02:00
Compare commits
44 Commits
Author | SHA1 | Date | |
---|---|---|---|
26ddc6e3aa | |||
1dc4a52f61 | |||
473a4fec70 | |||
1919c2d925 | |||
71e31e6c03 | |||
c01df7f0a1 | |||
6024f6175b | |||
33500e5b69 | |||
17899a6d6d | |||
4c3eb68d3a | |||
29ced9642d | |||
af82591d85 | |||
5bc4a446ec | |||
83e93b254e | |||
49c7dd0cac | |||
96d2fb62e4 | |||
c76a136d3f | |||
940409a4c3 | |||
071dd88ef8 | |||
a58a4634e2 | |||
5979e72662 | |||
010436e797 | |||
980709cccb | |||
fe80356756 | |||
cecf532ffd | |||
6cb255e60a | |||
b46fb7d1e1 | |||
8874193927 | |||
a4515ad251 | |||
55b0b57699 | |||
aab7795b4c | |||
196a8e6829 | |||
972cd98d7b | |||
a16b5d241b | |||
bfa918140f | |||
0721de5b81 | |||
a409fde519 | |||
8e34a30dce | |||
ba43462041 | |||
c8ae936ce9 | |||
853f949140 | |||
615b01a006 | |||
0eb5a3176b | |||
867a5a3ea0 |
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@ -3,7 +3,7 @@
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated:
|
||||
- To the latest version of the app (stable is v0.13.4)
|
||||
- To the latest version of the app (stable is v0.13.5)
|
||||
- All extensions
|
||||
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||
|
4
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
4
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
@ -53,7 +53,7 @@ body:
|
||||
label: Tachiyomi version
|
||||
description: You can find your Tachiyomi version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "0.13.4"
|
||||
Example: "0.13.5"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@ -98,7 +98,7 @@ body:
|
||||
required: true
|
||||
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
|
||||
required: true
|
||||
- label: I have updated the app to version **[0.13.4](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||
- label: I have updated the app to version **[0.13.5](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||
required: true
|
||||
- label: I have updated all installed extensions.
|
||||
required: true
|
||||
|
2
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
2
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
@ -33,7 +33,7 @@ body:
|
||||
required: true
|
||||
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
||||
required: true
|
||||
- label: I have updated the app to version **[0.13.4](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||
- label: I have updated the app to version **[0.13.5](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
3
.github/workflows/build_pull_request.yml
vendored
3
.github/workflows/build_pull_request.yml
vendored
@ -24,9 +24,10 @@ jobs:
|
||||
uses: actions/dependency-review-action@v1
|
||||
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 11
|
||||
distribution: adopt
|
||||
|
||||
- name: Copy CI gradle.properties
|
||||
run: |
|
||||
|
3
.github/workflows/build_push.yml
vendored
3
.github/workflows/build_push.yml
vendored
@ -25,9 +25,10 @@ jobs:
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 11
|
||||
distribution: adopt
|
||||
|
||||
- name: Copy CI gradle.properties
|
||||
run: |
|
||||
|
@ -24,8 +24,8 @@ android {
|
||||
applicationId = "eu.kanade.tachiyomi"
|
||||
minSdk = AndroidConfig.minSdk
|
||||
targetSdk = AndroidConfig.targetSdk
|
||||
versionCode = 80
|
||||
versionName = "0.13.4"
|
||||
versionCode = 81
|
||||
versionName = "0.13.5"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||
|
@ -55,7 +55,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||
|
||||
backup = Backup(
|
||||
backupManga(databaseManga, flags),
|
||||
backupCategories(),
|
||||
backupCategories(flags),
|
||||
emptyList(),
|
||||
backupExtensionInfo(databaseManga),
|
||||
)
|
||||
@ -133,10 +133,15 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||
*
|
||||
* @return list of [BackupCategory] to be backed up
|
||||
*/
|
||||
private fun backupCategories(): List<BackupCategory> {
|
||||
return databaseHelper.getCategories()
|
||||
.executeAsBlocking()
|
||||
.map { BackupCategory.copyFrom(it) }
|
||||
private fun backupCategories(options: Int): List<BackupCategory> {
|
||||
// Check if user wants category information in backup
|
||||
return if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
|
||||
databaseHelper.getCategories()
|
||||
.executeAsBlocking()
|
||||
.map { BackupCategory.copyFrom(it) }
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.download
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.core.app.NotificationCompat
|
||||
@ -187,16 +188,17 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
* @param timeout duration after which to automatically dismiss the notification.
|
||||
* Only works on Android 8+.
|
||||
*/
|
||||
fun onWarning(reason: String, timeout: Long? = null) {
|
||||
fun onWarning(reason: String, timeout: Long? = null, contentIntent: PendingIntent? = null) {
|
||||
with(errorNotificationBuilder) {
|
||||
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
||||
setContentText(reason)
|
||||
setStyle(NotificationCompat.BigTextStyle().bigText(reason))
|
||||
setSmallIcon(R.drawable.ic_warning_white_24dp)
|
||||
setAutoCancel(true)
|
||||
clearActions()
|
||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||
setProgress(0, 0, false)
|
||||
timeout?.let { setTimeoutAfter(it) }
|
||||
contentIntent?.let { setContentIntent(it) }
|
||||
|
||||
show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.download
|
||||
|
||||
import android.content.Context
|
||||
import android.webkit.MimeTypeMap
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
@ -11,6 +10,8 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateNotifier
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationHandler
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.UnmeteredSource
|
||||
@ -276,7 +277,8 @@ class Downloader(
|
||||
val maxDownloadsFromSource = queue
|
||||
.groupBy { it.source }
|
||||
.filterKeys { it !is UnmeteredSource }
|
||||
.maxOf { it.value.size }
|
||||
.maxOfOrNull { it.value.size }
|
||||
?: 0
|
||||
if (
|
||||
queuedDownloads > DOWNLOADS_QUEUED_WARNING_THRESHOLD ||
|
||||
maxDownloadsFromSource > CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD
|
||||
@ -285,6 +287,7 @@ class Downloader(
|
||||
notifier.onWarning(
|
||||
context.getString(R.string.download_queue_size_warning),
|
||||
WARNING_NOTIF_TIMEOUT_MS,
|
||||
NotificationHandler.openUrl(context, LibraryUpdateNotifier.HELP_WARNING_URL),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -468,7 +471,7 @@ class Downloader(
|
||||
// Else read magic numbers.
|
||||
?: ImageUtil.findImageType { file.openInputStream() }?.mime
|
||||
|
||||
return MimeTypeMap.getSingleton().getExtensionFromMimeType(mime) ?: "jpg"
|
||||
return ImageUtil.getExtensionFromMimeType(mime)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -8,9 +8,7 @@ import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import eu.kanade.tachiyomi.data.preference.DEVICE_CHARGING
|
||||
import eu.kanade.tachiyomi.data.preference.DEVICE_ONLY_ON_WIFI
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.*
|
||||
import eu.kanade.tachiyomi.util.system.isConnectedToWifi
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@ -21,8 +19,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
|
||||
override fun doWork(): Result {
|
||||
val preferences = Injekt.get<PreferencesHelper>()
|
||||
if (requiresWifiConnection(preferences) && !context.isConnectedToWifi()) {
|
||||
Result.failure()
|
||||
val restrictions = preferences.libraryUpdateDeviceRestriction().get()
|
||||
if ((DEVICE_ONLY_ON_WIFI in restrictions) && !context.isConnectedToWifi()) {
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
return if (LibraryUpdateService.start(context)) {
|
||||
@ -41,8 +40,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
if (interval > 0) {
|
||||
val restrictions = preferences.libraryUpdateDeviceRestriction().get()
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.setRequiredNetworkType(if (DEVICE_NETWORK_NOT_METERED in restrictions) { NetworkType.UNMETERED } else { NetworkType.CONNECTED })
|
||||
.setRequiresCharging(DEVICE_CHARGING in restrictions)
|
||||
.setRequiresBatteryNotLow(DEVICE_BATTERY_NOT_LOW in restrictions)
|
||||
.build()
|
||||
|
||||
val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>(
|
||||
@ -60,10 +60,5 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
|
||||
}
|
||||
}
|
||||
|
||||
fun requiresWifiConnection(preferences: PreferencesHelper): Boolean {
|
||||
val restrictions = preferences.libraryUpdateDeviceRestriction().get()
|
||||
return DEVICE_ONLY_ON_WIFI in restrictions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -93,9 +93,10 @@ class LibraryUpdateNotifier(private val context: Context) {
|
||||
fun showQueueSizeWarningNotification() {
|
||||
val notificationBuilder = context.notificationBuilder(Notifications.CHANNEL_LIBRARY_PROGRESS) {
|
||||
setContentTitle(context.getString(R.string.label_warning))
|
||||
setContentText(context.getString(R.string.notification_size_warning))
|
||||
setStyle(NotificationCompat.BigTextStyle().bigText(context.getString(R.string.notification_size_warning)))
|
||||
setSmallIcon(R.drawable.ic_warning_white_24dp)
|
||||
setTimeoutAfter(Downloader.WARNING_NOTIF_TIMEOUT_MS)
|
||||
setContentIntent(NotificationHandler.openUrl(context, HELP_WARNING_URL))
|
||||
}
|
||||
|
||||
context.notificationManager.notify(
|
||||
@ -340,6 +341,10 @@ class LibraryUpdateNotifier(private val context: Context) {
|
||||
}
|
||||
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val HELP_WARNING_URL = "https://tachiyomi.org/help/faq/#why-does-the-app-warn-about-large-bulk-updates-and-downloads"
|
||||
}
|
||||
}
|
||||
|
||||
private const val NOTIF_MAX_CHAPTERS = 5
|
||||
|
@ -174,6 +174,8 @@ class LibraryUpdateService(
|
||||
*/
|
||||
override fun onDestroy() {
|
||||
updateJob?.cancel()
|
||||
// Despite what Android Studio
|
||||
// states this can be null
|
||||
ioScope?.cancel()
|
||||
if (wakeLock.isHeld) {
|
||||
wakeLock.release()
|
||||
@ -233,8 +235,7 @@ class LibraryUpdateService(
|
||||
/**
|
||||
* Adds list of manga to be updated.
|
||||
*
|
||||
* @param category the ID of the category to update, or -1 if no category specified.
|
||||
* @param target the target to update.
|
||||
* @param categoryId the ID of the category to update, or -1 if no category specified.
|
||||
*/
|
||||
fun addMangaToQueue(categoryId: Int) {
|
||||
val libraryManga = db.getLibraryMangas().executeAsBlocking()
|
||||
@ -274,12 +275,11 @@ class LibraryUpdateService(
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that updates the given list of manga. It's called in a background thread, so it's safe
|
||||
* Method that updates manga in [mangaToUpdate]. It's called in a background thread, so it's safe
|
||||
* to do heavy operations or network calls here.
|
||||
* For each manga it calls [updateManga] and updates the notification showing the current
|
||||
* progress.
|
||||
*
|
||||
* @param mangaToUpdate the list to update
|
||||
* @return an observable delivering the progress of each update.
|
||||
*/
|
||||
suspend fun updateChapterList() {
|
||||
@ -305,35 +305,38 @@ class LibraryUpdateService(
|
||||
return@async
|
||||
}
|
||||
|
||||
// Don't continue to update if manga not in library
|
||||
db.getManga(manga.id!!).executeAsBlocking() ?: return@forEach
|
||||
|
||||
withUpdateNotification(
|
||||
currentlyUpdatingManga,
|
||||
progressCount,
|
||||
manga,
|
||||
) { manga ->
|
||||
) { mangaWithNotif ->
|
||||
try {
|
||||
when {
|
||||
MANGA_NON_COMPLETED in restrictions && manga.status == SManga.COMPLETED -> {
|
||||
skippedUpdates.add(manga to getString(R.string.skipped_reason_completed))
|
||||
}
|
||||
MANGA_HAS_UNREAD in restrictions && manga.unreadCount != 0 -> {
|
||||
skippedUpdates.add(manga to getString(R.string.skipped_reason_not_caught_up))
|
||||
}
|
||||
MANGA_NON_READ in restrictions && manga.totalChapters > 0 && !manga.hasStarted -> {
|
||||
skippedUpdates.add(manga to getString(R.string.skipped_reason_not_started))
|
||||
}
|
||||
MANGA_NON_COMPLETED in restrictions && mangaWithNotif.status == SManga.COMPLETED ->
|
||||
skippedUpdates.add(mangaWithNotif to getString(R.string.skipped_reason_completed))
|
||||
|
||||
MANGA_HAS_UNREAD in restrictions && mangaWithNotif.unreadCount != 0 ->
|
||||
skippedUpdates.add(mangaWithNotif to getString(R.string.skipped_reason_not_caught_up))
|
||||
|
||||
MANGA_NON_READ in restrictions && mangaWithNotif.totalChapters > 0 && !mangaWithNotif.hasStarted ->
|
||||
skippedUpdates.add(mangaWithNotif to getString(R.string.skipped_reason_not_started))
|
||||
|
||||
else -> {
|
||||
// Convert to the manga that contains new chapters
|
||||
val (newChapters, _) = updateManga(manga)
|
||||
val (newChapters, _) = updateManga(mangaWithNotif)
|
||||
|
||||
if (newChapters.isNotEmpty()) {
|
||||
if (manga.shouldDownloadNewChapters(db, preferences)) {
|
||||
downloadChapters(manga, newChapters)
|
||||
if (mangaWithNotif.shouldDownloadNewChapters(db, preferences)) {
|
||||
downloadChapters(mangaWithNotif, newChapters)
|
||||
hasDownloads.set(true)
|
||||
}
|
||||
|
||||
// Convert to the manga that contains new chapters
|
||||
newUpdates.add(
|
||||
manga to newChapters.sortedByDescending { ch -> ch.source_order }
|
||||
mangaWithNotif to newChapters.sortedByDescending { ch -> ch.source_order }
|
||||
.toTypedArray(),
|
||||
)
|
||||
}
|
||||
@ -352,11 +355,11 @@ class LibraryUpdateService(
|
||||
e.message
|
||||
}
|
||||
}
|
||||
failedUpdates.add(manga to errorMessage)
|
||||
failedUpdates.add(mangaWithNotif to errorMessage)
|
||||
}
|
||||
|
||||
if (preferences.autoUpdateTrackers()) {
|
||||
updateTrackings(manga, loggedServices)
|
||||
updateTrackings(mangaWithNotif, loggedServices)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -404,6 +407,7 @@ class LibraryUpdateService(
|
||||
suspend fun updateManga(manga: Manga): Pair<List<Chapter>, List<Chapter>> {
|
||||
val source = sourceManager.getOrStub(manga.source)
|
||||
|
||||
var networkSManga: SManga? = null
|
||||
// Update manga details metadata
|
||||
if (preferences.autoUpdateMetadata()) {
|
||||
val updatedManga = source.getMangaDetails(manga.toMangaInfo())
|
||||
@ -415,14 +419,26 @@ class LibraryUpdateService(
|
||||
sManga.thumbnail_url = manga.thumbnail_url
|
||||
}
|
||||
|
||||
manga.copyFrom(sManga)
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
networkSManga = sManga
|
||||
}
|
||||
|
||||
val chapters = source.getChapterList(manga.toMangaInfo())
|
||||
.map { it.toSChapter() }
|
||||
|
||||
return syncChaptersWithSource(db, chapters, manga, source)
|
||||
// Get manga from database to account for if it was removed
|
||||
// from library or database
|
||||
val dbManga = db.getManga(manga.id!!).executeAsBlocking()
|
||||
?: return Pair(emptyList(), emptyList())
|
||||
|
||||
// Copy into [dbManga] to retain favourite value
|
||||
networkSManga?.let {
|
||||
dbManga.copyFrom(it)
|
||||
db.insertManga(dbManga).executeAsBlocking()
|
||||
}
|
||||
|
||||
// [dbmanga] was used so that manga data doesn't get overwritten
|
||||
// incase manga gets new chapter
|
||||
return syncChaptersWithSource(db, chapters, dbManga, source)
|
||||
}
|
||||
|
||||
private suspend fun updateCovers() {
|
||||
@ -445,16 +461,16 @@ class LibraryUpdateService(
|
||||
currentlyUpdatingManga,
|
||||
progressCount,
|
||||
manga,
|
||||
) { manga ->
|
||||
sourceManager.get(manga.source)?.let { source ->
|
||||
) { mangaWithNotif ->
|
||||
sourceManager.get(mangaWithNotif.source)?.let { source ->
|
||||
try {
|
||||
val networkManga =
|
||||
source.getMangaDetails(manga.toMangaInfo())
|
||||
source.getMangaDetails(mangaWithNotif.toMangaInfo())
|
||||
val sManga = networkManga.toSManga()
|
||||
manga.prepUpdateCover(coverCache, sManga, true)
|
||||
mangaWithNotif.prepUpdateCover(coverCache, sManga, true)
|
||||
sManga.thumbnail_url?.let {
|
||||
manga.thumbnail_url = it
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
mangaWithNotif.thumbnail_url = it
|
||||
db.insertManga(mangaWithNotif).executeAsBlocking()
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
// Ignore errors and continue
|
||||
|
@ -30,7 +30,7 @@ object Notifications {
|
||||
const val CHANNEL_LIBRARY_ERROR = "library_errors_channel"
|
||||
const val ID_LIBRARY_ERROR = -102
|
||||
const val CHANNEL_LIBRARY_SKIPPED = "library_skipped_channel"
|
||||
const val ID_LIBRARY_SKIPPED = -103
|
||||
const val ID_LIBRARY_SKIPPED = -104
|
||||
|
||||
/**
|
||||
* Notification channel and ids used by the downloader.
|
||||
|
@ -3,7 +3,9 @@ package eu.kanade.tachiyomi.data.preference
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
const val DEVICE_ONLY_ON_WIFI = "wifi"
|
||||
const val DEVICE_NETWORK_NOT_METERED = "network_not_metered"
|
||||
const val DEVICE_CHARGING = "ac"
|
||||
const val DEVICE_BATTERY_NOT_LOW = "battery_not_low"
|
||||
|
||||
const val MANGA_NON_COMPLETED = "manga_ongoing"
|
||||
const val MANGA_HAS_UNREAD = "manga_fully_read"
|
||||
@ -28,13 +30,14 @@ object PreferenceValues {
|
||||
enum class AppTheme(val titleResId: Int?) {
|
||||
DEFAULT(R.string.label_default),
|
||||
MONET(R.string.theme_monet),
|
||||
GREEN_APPLE(R.string.theme_greenapple),
|
||||
LAVENDER(R.string.theme_lavender),
|
||||
MIDNIGHT_DUSK(R.string.theme_midnightdusk),
|
||||
STRAWBERRY_DAIQUIRI(R.string.theme_strawberrydaiquiri),
|
||||
YOTSUBA(R.string.theme_yotsuba),
|
||||
TAKO(R.string.theme_tako),
|
||||
GREEN_APPLE(R.string.theme_greenapple),
|
||||
TEALTURQUOISE(R.string.theme_tealturquoise),
|
||||
YINYANG(R.string.theme_yinyang),
|
||||
YOTSUBA(R.string.theme_yotsuba),
|
||||
|
||||
// Deprecated
|
||||
DARK_BLUE(null),
|
||||
|
@ -204,7 +204,7 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true)
|
||||
|
||||
fun saveChaptersAsCBZ() = flowPrefs.getBoolean("save_chapter_as_cbz", false)
|
||||
fun saveChaptersAsCBZ() = flowPrefs.getBoolean("save_chapter_as_cbz", true)
|
||||
|
||||
fun folderPerManga() = prefs.getBoolean(Keys.folderPerManga, false)
|
||||
|
||||
@ -278,10 +278,10 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun pinnedSources() = flowPrefs.getStringSet("pinned_catalogues", emptySet())
|
||||
|
||||
fun downloadNew() = flowPrefs.getBoolean("download_new", false)
|
||||
fun downloadNewChapter() = flowPrefs.getBoolean("download_new", false)
|
||||
|
||||
fun downloadNewCategories() = flowPrefs.getStringSet("download_new_categories", emptySet())
|
||||
fun downloadNewCategoriesExclude() = flowPrefs.getStringSet("download_new_categories_exclude", emptySet())
|
||||
fun downloadNewChapterCategories() = flowPrefs.getStringSet("download_new_categories", emptySet())
|
||||
fun downloadNewChapterCategoriesExclude() = flowPrefs.getStringSet("download_new_categories_exclude", emptySet())
|
||||
|
||||
fun defaultCategory() = prefs.getInt(Keys.defaultCategory, -1)
|
||||
|
||||
|
@ -56,6 +56,7 @@ class AppUpdateChecker {
|
||||
private fun isNewVersion(versionTag: String): Boolean {
|
||||
// Removes prefixes like "r" or "v"
|
||||
val newVersion = versionTag.replace("[^\\d.]".toRegex(), "")
|
||||
val oldVersion = BuildConfig.VERSION_NAME.replace("[^\\d.]".toRegex(), "")
|
||||
|
||||
return if (BuildConfig.PREVIEW) {
|
||||
// Preview builds: based on releases in "tachiyomiorg/tachiyomi-preview" repo
|
||||
@ -64,7 +65,15 @@ class AppUpdateChecker {
|
||||
} else {
|
||||
// Release builds: based on releases in "tachiyomiorg/tachiyomi" repo
|
||||
// tagged as something like "v0.1.2"
|
||||
newVersion != BuildConfig.VERSION_NAME
|
||||
val newSemVer = newVersion.split(".").map { it.toInt() }
|
||||
val oldSemVer = oldVersion.split(".").map { it.toInt() }
|
||||
|
||||
oldSemVer.mapIndexed { index, i ->
|
||||
if (newSemVer[index] > i) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,9 @@ import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import kotlinx.serialization.Serializable
|
||||
import logcat.LogPriority
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.Date
|
||||
import java.util.concurrent.TimeUnit
|
||||
@ -21,11 +23,27 @@ internal class ExtensionGithubApi {
|
||||
private val networkService: NetworkHelper by injectLazy()
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
private var requiresFallbackSource = false
|
||||
|
||||
suspend fun findExtensions(): List<Extension.Available> {
|
||||
return withIOContext {
|
||||
val extensions = networkService.client
|
||||
.newCall(GET("${REPO_URL_PREFIX}index.min.json"))
|
||||
.await()
|
||||
val githubResponse = if (requiresFallbackSource) null else try {
|
||||
networkService.client
|
||||
.newCall(GET("${REPO_URL_PREFIX}index.min.json"))
|
||||
.await()
|
||||
} catch (e: Throwable) {
|
||||
logcat(LogPriority.ERROR, e) { "Failed to get extensions from GitHub" }
|
||||
requiresFallbackSource = true
|
||||
null
|
||||
}
|
||||
|
||||
val response = githubResponse ?: run {
|
||||
networkService.client
|
||||
.newCall(GET("${FALLBACK_REPO_URL_PREFIX}index.min.json"))
|
||||
.await()
|
||||
}
|
||||
|
||||
val extensions = response
|
||||
.parseAs<List<ExtensionJsonObject>>()
|
||||
.toExtensions()
|
||||
|
||||
@ -85,7 +103,7 @@ internal class ExtensionGithubApi {
|
||||
hasChangelog = it.hasChangelog == 1,
|
||||
sources = it.sources?.toExtensionSources() ?: emptyList(),
|
||||
apkName = it.apk,
|
||||
iconUrl = "${REPO_URL_PREFIX}icon/${it.apk.replace(".apk", ".png")}",
|
||||
iconUrl = "${getUrlPrefix()}icon/${it.apk.replace(".apk", ".png")}",
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -101,11 +119,20 @@ internal class ExtensionGithubApi {
|
||||
}
|
||||
|
||||
fun getApkUrl(extension: Extension.Available): String {
|
||||
return "${REPO_URL_PREFIX}apk/${extension.apkName}"
|
||||
return "${getUrlPrefix()}apk/${extension.apkName}"
|
||||
}
|
||||
|
||||
private fun getUrlPrefix(): String {
|
||||
return if (requiresFallbackSource) {
|
||||
FALLBACK_REPO_URL_PREFIX
|
||||
} else {
|
||||
REPO_URL_PREFIX
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val REPO_URL_PREFIX = "https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/"
|
||||
private const val FALLBACK_REPO_URL_PREFIX = "https://gcore.jsdelivr.net/gh/tachiyomiorg/tachiyomi-extensions@repo/"
|
||||
|
||||
@Serializable
|
||||
private data class ExtensionJsonObject(
|
||||
|
@ -52,9 +52,9 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
|
||||
val size = service.getUriSize(entry.uri) ?: throw IllegalStateException()
|
||||
service.contentResolver.openInputStream(entry.uri)!!.use {
|
||||
val createCommand = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
"pm install-create --user current -i ${service.packageName} -S $size"
|
||||
"pm install-create --user current -r -i ${service.packageName} -S $size"
|
||||
} else {
|
||||
"pm install-create -i ${service.packageName} -S $size"
|
||||
"pm install-create -r -i ${service.packageName} -S $size"
|
||||
}
|
||||
val createResult = exec(createCommand)
|
||||
sessionId = SESSION_ID_REGEX.find(createResult.out)?.value
|
||||
|
@ -13,6 +13,10 @@ const val PREF_DOH_CLOUDFLARE = 1
|
||||
const val PREF_DOH_GOOGLE = 2
|
||||
const val PREF_DOH_ADGUARD = 3
|
||||
const val PREF_DOH_QUAD9 = 4
|
||||
const val PREF_DOH_ALIDNS = 5
|
||||
const val PREF_DOH_DNSPOD = 6
|
||||
const val PREF_DOH_360 = 7
|
||||
const val PREF_DOH_QUAD101 = 8
|
||||
|
||||
fun OkHttpClient.Builder.dohCloudflare() = dns(
|
||||
DnsOverHttps.Builder().client(build())
|
||||
@ -68,3 +72,51 @@ fun OkHttpClient.Builder.dohQuad9() = dns(
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
|
||||
fun OkHttpClient.Builder.dohAliDNS() = dns(
|
||||
DnsOverHttps.Builder().client(build())
|
||||
.url("https://dns.alidns.com/dns-query".toHttpUrl())
|
||||
.bootstrapDnsHosts(
|
||||
InetAddress.getByName("223.5.5.5"),
|
||||
InetAddress.getByName("223.6.6.6"),
|
||||
InetAddress.getByName("2400:3200::1"),
|
||||
InetAddress.getByName("2400:3200:baba::1"),
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
|
||||
fun OkHttpClient.Builder.dohDNSPod() = dns(
|
||||
DnsOverHttps.Builder().client(build())
|
||||
.url("https://doh.pub/dns-query".toHttpUrl())
|
||||
.bootstrapDnsHosts(
|
||||
InetAddress.getByName("1.12.12.12"),
|
||||
InetAddress.getByName("120.53.53.53"),
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
|
||||
fun OkHttpClient.Builder.doh360() = dns(
|
||||
DnsOverHttps.Builder().client(build())
|
||||
.url("https://doh.360.cn/dns-query".toHttpUrl())
|
||||
.bootstrapDnsHosts(
|
||||
InetAddress.getByName("101.226.4.6"),
|
||||
InetAddress.getByName("218.30.118.6"),
|
||||
InetAddress.getByName("123.125.81.6"),
|
||||
InetAddress.getByName("140.207.198.6"),
|
||||
InetAddress.getByName("180.163.249.75"),
|
||||
InetAddress.getByName("101.199.113.208"),
|
||||
InetAddress.getByName("36.99.170.86"),
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
|
||||
fun OkHttpClient.Builder.dohQuad101() = dns(
|
||||
DnsOverHttps.Builder().client(build())
|
||||
.url("https://dns.twnic.tw/dns-query".toHttpUrl())
|
||||
.bootstrapDnsHosts(
|
||||
InetAddress.getByName("101.101.101.101"),
|
||||
InetAddress.getByName("2001:de4::101"),
|
||||
InetAddress.getByName("2001:de4::102"),
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
|
@ -43,6 +43,10 @@ class NetworkHelper(context: Context) {
|
||||
PREF_DOH_GOOGLE -> builder.dohGoogle()
|
||||
PREF_DOH_ADGUARD -> builder.dohAdGuard()
|
||||
PREF_DOH_QUAD9 -> builder.dohQuad9()
|
||||
PREF_DOH_ALIDNS -> builder.dohAliDNS()
|
||||
PREF_DOH_DNSPOD -> builder.dohDNSPod()
|
||||
PREF_DOH_360 -> builder.doh360()
|
||||
PREF_DOH_QUAD101 -> builder.dohQuad101()
|
||||
}
|
||||
|
||||
return builder
|
||||
|
@ -4,6 +4,7 @@ import android.os.SystemClock
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
@ -36,6 +37,11 @@ private class RateLimitInterceptor(
|
||||
private val rateLimitMillis = unit.toMillis(period)
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
// Ignore canceled calls, otherwise they would jam the queue
|
||||
if (chain.call().isCanceled()) {
|
||||
throw IOException()
|
||||
}
|
||||
|
||||
synchronized(requestQueue) {
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
val waitTime = if (requestQueue.size < permits) {
|
||||
@ -51,6 +57,11 @@ private class RateLimitInterceptor(
|
||||
}
|
||||
}
|
||||
|
||||
// Final check
|
||||
if (chain.call().isCanceled()) {
|
||||
throw IOException()
|
||||
}
|
||||
|
||||
if (requestQueue.size == permits) {
|
||||
requestQueue.removeAt(0)
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import okhttp3.HttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
@ -41,9 +42,13 @@ class SpecificHostRateLimitInterceptor(
|
||||
private val host = httpUrl.host
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
if (chain.request().url.host != host) {
|
||||
// Ignore canceled calls, otherwise they would jam the queue
|
||||
if (chain.call().isCanceled()) {
|
||||
throw IOException()
|
||||
} else if (chain.request().url.host != host) {
|
||||
return chain.proceed(chain.request())
|
||||
}
|
||||
|
||||
synchronized(requestQueue) {
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
val waitTime = if (requestQueue.size < permits) {
|
||||
@ -59,6 +64,11 @@ class SpecificHostRateLimitInterceptor(
|
||||
}
|
||||
}
|
||||
|
||||
// Final check
|
||||
if (chain.call().isCanceled()) {
|
||||
throw IOException()
|
||||
}
|
||||
|
||||
if (requestQueue.size == permits) {
|
||||
requestQueue.removeAt(0)
|
||||
}
|
||||
|
@ -2,7 +2,9 @@ package eu.kanade.tachiyomi.source
|
||||
|
||||
import android.content.Context
|
||||
import com.github.junrar.Archive
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
@ -17,7 +19,6 @@ import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.storage.EpubFile
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
@ -26,10 +27,11 @@ import kotlinx.serialization.json.decodeFromStream
|
||||
import kotlinx.serialization.json.intOrNull
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import logcat.LogPriority
|
||||
import rx.Observable
|
||||
import tachiyomi.source.model.ChapterInfo
|
||||
import tachiyomi.source.model.MangaInfo
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
@ -37,130 +39,104 @@ import java.io.InputStream
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSource {
|
||||
|
||||
companion object {
|
||||
const val ID = 0L
|
||||
const val HELP_URL = "https://tachiyomi.org/help/guides/local-manga/"
|
||||
|
||||
private const val COVER_NAME = "cover.jpg"
|
||||
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
|
||||
|
||||
fun updateCover(context: Context, manga: SManga, input: InputStream): File? {
|
||||
val dir = getBaseDirectories(context).firstOrNull()
|
||||
if (dir == null) {
|
||||
input.close()
|
||||
return null
|
||||
}
|
||||
var cover = getCoverFile(File("${dir.absolutePath}/${manga.url}"))
|
||||
if (cover == null) {
|
||||
cover = File("${dir.absolutePath}/${manga.url}", COVER_NAME)
|
||||
}
|
||||
// It might not exist if using the external SD card
|
||||
cover.parentFile?.mkdirs()
|
||||
input.use {
|
||||
cover.outputStream().use {
|
||||
input.copyTo(it)
|
||||
}
|
||||
}
|
||||
manga.thumbnail_url = cover.absolutePath
|
||||
return cover
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns valid cover file inside [parent] directory.
|
||||
*/
|
||||
private fun getCoverFile(parent: File): File? {
|
||||
return parent.listFiles()?.find { it.nameWithoutExtension == "cover" }?.takeIf {
|
||||
it.isFile && ImageUtil.isImage(it.name) { it.inputStream() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBaseDirectories(context: Context): List<File> {
|
||||
val c = context.getString(R.string.app_name) + File.separator + "local"
|
||||
return DiskUtil.getExternalStorages(context).map { File(it.absolutePath, c) }
|
||||
}
|
||||
}
|
||||
class LocalSource(
|
||||
private val context: Context,
|
||||
private val coverCache: CoverCache = Injekt.get(),
|
||||
) : CatalogueSource, UnmeteredSource {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
override val id = ID
|
||||
override val name = context.getString(R.string.local_source)
|
||||
override val lang = "other"
|
||||
override val supportsLatest = true
|
||||
override val name: String = context.getString(R.string.local_source)
|
||||
|
||||
override val id: Long = ID
|
||||
|
||||
override val lang: String = "other"
|
||||
|
||||
override fun toString() = name
|
||||
|
||||
override val supportsLatest: Boolean = true
|
||||
|
||||
// Browse related
|
||||
override fun fetchPopularManga(page: Int) = fetchSearchManga(page, "", POPULAR_FILTERS)
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
val baseDirs = getBaseDirectories(context)
|
||||
override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
|
||||
|
||||
val time = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
|
||||
var mangaDirs = baseDirs
|
||||
.asSequence()
|
||||
.mapNotNull { it.listFiles()?.toList() }
|
||||
.flatten()
|
||||
.filter { it.isDirectory }
|
||||
.filterNot { it.name.startsWith('.') }
|
||||
.filter { if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time }
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
val baseDirsFiles = getBaseDirectoriesFiles(context)
|
||||
|
||||
var mangaDirs = baseDirsFiles
|
||||
// Filter out files that are hidden and is not a folder
|
||||
.filter { it.isDirectory && !it.name.startsWith('.') }
|
||||
.distinctBy { it.name }
|
||||
|
||||
val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state
|
||||
when (state?.index) {
|
||||
0 -> {
|
||||
mangaDirs = if (state.ascending) {
|
||||
mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, { it.name }))
|
||||
} else {
|
||||
mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER, { it.name }))
|
||||
}
|
||||
}
|
||||
1 -> {
|
||||
mangaDirs = if (state.ascending) {
|
||||
mangaDirs.sortedBy(File::lastModified)
|
||||
} else {
|
||||
mangaDirs.sortedByDescending(File::lastModified)
|
||||
}
|
||||
val lastModifiedLimit = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
|
||||
// Filter by query or last modified
|
||||
mangaDirs = mangaDirs.filter {
|
||||
if (lastModifiedLimit == 0L) {
|
||||
it.name.contains(query, ignoreCase = true)
|
||||
} else {
|
||||
it.lastModified() >= lastModifiedLimit
|
||||
}
|
||||
}
|
||||
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is OrderBy -> {
|
||||
when (filter.state!!.index) {
|
||||
0 -> {
|
||||
mangaDirs = if (filter.state!!.ascending) {
|
||||
mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||
} else {
|
||||
mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||
}
|
||||
}
|
||||
1 -> {
|
||||
mangaDirs = if (filter.state!!.ascending) {
|
||||
mangaDirs.sortedBy(File::lastModified)
|
||||
} else {
|
||||
mangaDirs.sortedByDescending(File::lastModified)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> { /* Do nothing */ }
|
||||
}
|
||||
}
|
||||
|
||||
// Transform mangaDirs to list of SManga
|
||||
val mangas = mangaDirs.map { mangaDir ->
|
||||
SManga.create().apply {
|
||||
title = mangaDir.name
|
||||
url = mangaDir.name
|
||||
|
||||
// Try to find the cover
|
||||
for (dir in baseDirs) {
|
||||
val cover = getCoverFile(File("${dir.absolutePath}/$url"))
|
||||
if (cover != null && cover.exists()) {
|
||||
thumbnail_url = cover.absolutePath
|
||||
break
|
||||
}
|
||||
val cover = getCoverFile(mangaDir.name, baseDirsFiles)
|
||||
if (cover != null && cover.exists()) {
|
||||
thumbnail_url = cover.absolutePath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val sManga = this
|
||||
val mangaInfo = this.toMangaInfo()
|
||||
runBlocking {
|
||||
val chapters = getChapterList(mangaInfo)
|
||||
if (chapters.isNotEmpty()) {
|
||||
val chapter = chapters.last().toSChapter()
|
||||
val format = getFormat(chapter)
|
||||
if (format is Format.Epub) {
|
||||
EpubFile(format.file).use { epub ->
|
||||
epub.fillMangaMetadata(sManga)
|
||||
}
|
||||
}
|
||||
// Fetch chapters of all the manga
|
||||
mangas.forEach { manga ->
|
||||
val mangaInfo = manga.toMangaInfo()
|
||||
runBlocking {
|
||||
val chapters = getChapterList(mangaInfo)
|
||||
if (chapters.isNotEmpty()) {
|
||||
val chapter = chapters.last().toSChapter()
|
||||
val format = getFormat(chapter)
|
||||
|
||||
// Copy the cover from the first chapter found.
|
||||
if (thumbnail_url == null) {
|
||||
try {
|
||||
val dest = updateCover(chapter, sManga)
|
||||
thumbnail_url = dest?.absolutePath
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
}
|
||||
if (format is Format.Epub) {
|
||||
EpubFile(format.file).use { epub ->
|
||||
epub.fillMangaMetadata(manga)
|
||||
}
|
||||
}
|
||||
|
||||
// Copy the cover from the first chapter found if not available
|
||||
if (manga.thumbnail_url == null) {
|
||||
updateCover(chapter, manga)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -168,38 +144,44 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
||||
return Observable.just(MangasPage(mangas.toList(), false))
|
||||
}
|
||||
|
||||
override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
|
||||
|
||||
// Manga details related
|
||||
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
|
||||
val localDetails = getBaseDirectories(context)
|
||||
.asSequence()
|
||||
.mapNotNull { File(it, manga.key).listFiles()?.toList() }
|
||||
.flatten()
|
||||
var mangaInfo = manga
|
||||
|
||||
val baseDirsFile = getBaseDirectoriesFiles(context)
|
||||
|
||||
val coverFile = getCoverFile(manga.key, baseDirsFile)
|
||||
|
||||
coverFile?.let {
|
||||
mangaInfo = mangaInfo.copy(cover = it.absolutePath)
|
||||
}
|
||||
|
||||
val localDetails = getMangaDirsFiles(manga.key, baseDirsFile)
|
||||
.firstOrNull { it.extension.equals("json", ignoreCase = true) }
|
||||
|
||||
return if (localDetails != null) {
|
||||
if (localDetails != null) {
|
||||
val obj = json.decodeFromStream<JsonObject>(localDetails.inputStream())
|
||||
|
||||
manga.copy(
|
||||
title = obj["title"]?.jsonPrimitive?.contentOrNull ?: manga.title,
|
||||
author = obj["author"]?.jsonPrimitive?.contentOrNull ?: manga.author,
|
||||
artist = obj["artist"]?.jsonPrimitive?.contentOrNull ?: manga.artist,
|
||||
description = obj["description"]?.jsonPrimitive?.contentOrNull ?: manga.description,
|
||||
genres = obj["genre"]?.jsonArray?.map { it.jsonPrimitive.content } ?: manga.genres,
|
||||
status = obj["status"]?.jsonPrimitive?.intOrNull ?: manga.status,
|
||||
mangaInfo = mangaInfo.copy(
|
||||
title = obj["title"]?.jsonPrimitive?.contentOrNull ?: mangaInfo.title,
|
||||
author = obj["author"]?.jsonPrimitive?.contentOrNull ?: mangaInfo.author,
|
||||
artist = obj["artist"]?.jsonPrimitive?.contentOrNull ?: mangaInfo.artist,
|
||||
description = obj["description"]?.jsonPrimitive?.contentOrNull ?: mangaInfo.description,
|
||||
genres = obj["genre"]?.jsonArray?.map { it.jsonPrimitive.content } ?: mangaInfo.genres,
|
||||
status = obj["status"]?.jsonPrimitive?.intOrNull ?: mangaInfo.status,
|
||||
)
|
||||
} else {
|
||||
manga
|
||||
}
|
||||
|
||||
return mangaInfo
|
||||
}
|
||||
|
||||
// Chapters
|
||||
override suspend fun getChapterList(manga: MangaInfo): List<ChapterInfo> {
|
||||
val sManga = manga.toSManga()
|
||||
|
||||
val chapters = getBaseDirectories(context)
|
||||
.asSequence()
|
||||
.mapNotNull { File(it, manga.key).listFiles()?.toList() }
|
||||
.flatten()
|
||||
val baseDirsFile = getBaseDirectoriesFiles(context)
|
||||
return getMangaDirsFiles(manga.key, baseDirsFile)
|
||||
// Only keep supported formats
|
||||
.filter { it.isDirectory || isSupportedFile(it.extension) }
|
||||
.map { chapterFile ->
|
||||
SChapter.create().apply {
|
||||
@ -211,14 +193,14 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
||||
}
|
||||
date_upload = chapterFile.lastModified()
|
||||
|
||||
ChapterRecognition.parseChapterNumber(this, sManga)
|
||||
|
||||
val format = getFormat(chapterFile)
|
||||
if (format is Format.Epub) {
|
||||
EpubFile(format.file).use { epub ->
|
||||
epub.fillChapterMetadata(this)
|
||||
}
|
||||
}
|
||||
|
||||
ChapterRecognition.parseChapterNumber(this, sManga)
|
||||
}
|
||||
}
|
||||
.map { it.toChapterInfo() }
|
||||
@ -227,12 +209,24 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
||||
if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c
|
||||
}
|
||||
.toList()
|
||||
|
||||
return chapters
|
||||
}
|
||||
|
||||
override suspend fun getPageList(chapter: ChapterInfo) = throw Exception("Unused")
|
||||
// Filters
|
||||
override fun getFilterList() = FilterList(OrderBy(context))
|
||||
|
||||
private val POPULAR_FILTERS = FilterList(OrderBy(context))
|
||||
private val LATEST_FILTERS = FilterList(OrderBy(context).apply { state = Filter.Sort.Selection(1, false) })
|
||||
|
||||
private class OrderBy(context: Context) : Filter.Sort(
|
||||
context.getString(R.string.local_filter_order_by),
|
||||
arrayOf(context.getString(R.string.title), context.getString(R.string.date)),
|
||||
Selection(0, true),
|
||||
)
|
||||
|
||||
// Unused stuff
|
||||
override suspend fun getPageList(chapter: ChapterInfo) = throw UnsupportedOperationException("Unused")
|
||||
|
||||
// Miscellaneous
|
||||
private fun isSupportedFile(extension: String): Boolean {
|
||||
return extension.lowercase() in SUPPORTED_ARCHIVE_TYPES
|
||||
}
|
||||
@ -296,25 +290,89 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
||||
}
|
||||
}
|
||||
}
|
||||
.also { coverCache.clearMemoryCache() }
|
||||
}
|
||||
|
||||
override fun getFilterList() = POPULAR_FILTERS
|
||||
|
||||
private val POPULAR_FILTERS = FilterList(OrderBy(context))
|
||||
private val LATEST_FILTERS = FilterList(OrderBy(context).apply { state = Filter.Sort.Selection(1, false) })
|
||||
|
||||
private class OrderBy(context: Context) : Filter.Sort(
|
||||
context.getString(R.string.local_filter_order_by),
|
||||
arrayOf(context.getString(R.string.title), context.getString(R.string.date)),
|
||||
Selection(0, true),
|
||||
)
|
||||
|
||||
sealed class Format {
|
||||
data class Directory(val file: File) : Format()
|
||||
data class Zip(val file: File) : Format()
|
||||
data class Rar(val file: File) : Format()
|
||||
data class Epub(val file: File) : Format()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ID = 0L
|
||||
const val HELP_URL = "https://tachiyomi.org/help/guides/local-manga/"
|
||||
|
||||
private const val DEFAULT_COVER_NAME = "cover.jpg"
|
||||
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
|
||||
|
||||
private fun getBaseDirectories(context: Context): Sequence<File> {
|
||||
val localFolder = context.getString(R.string.app_name) + File.separator + "local"
|
||||
return DiskUtil.getExternalStorages(context)
|
||||
.map { File(it.absolutePath, localFolder) }
|
||||
.asSequence()
|
||||
}
|
||||
|
||||
private fun getBaseDirectoriesFiles(context: Context): Sequence<File> {
|
||||
return getBaseDirectories(context)
|
||||
// Get all the files inside all baseDir
|
||||
.flatMap { it.listFiles().orEmpty().toList() }
|
||||
}
|
||||
|
||||
private fun getMangaDir(mangaUrl: String, baseDirsFile: Sequence<File>): File? {
|
||||
return baseDirsFile
|
||||
// Get the first mangaDir or null
|
||||
.firstOrNull { it.isDirectory && it.name == mangaUrl }
|
||||
}
|
||||
|
||||
private fun getMangaDirsFiles(mangaUrl: String, baseDirsFile: Sequence<File>): Sequence<File> {
|
||||
return baseDirsFile
|
||||
// Filter out ones that are not related to the manga and is not a directory
|
||||
.filter { it.isDirectory && it.name == mangaUrl }
|
||||
// Get all the files inside the filtered folders
|
||||
.flatMap { it.listFiles().orEmpty().toList() }
|
||||
}
|
||||
|
||||
private fun getCoverFile(mangaUrl: String, baseDirsFile: Sequence<File>): File? {
|
||||
return getMangaDirsFiles(mangaUrl, baseDirsFile)
|
||||
// Get all file whose names start with 'cover'
|
||||
.filter { it.isFile && it.nameWithoutExtension.equals("cover", ignoreCase = true) }
|
||||
// Get the first actual image
|
||||
.firstOrNull {
|
||||
ImageUtil.isImage(it.name) { it.inputStream() }
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCover(context: Context, manga: SManga, inputStream: InputStream): File? {
|
||||
val baseDirsFiles = getBaseDirectoriesFiles(context)
|
||||
|
||||
val mangaDir = getMangaDir(manga.url, baseDirsFiles)
|
||||
if (mangaDir == null) {
|
||||
inputStream.close()
|
||||
return null
|
||||
}
|
||||
|
||||
var coverFile = getCoverFile(manga.url, baseDirsFiles)
|
||||
if (coverFile == null) {
|
||||
coverFile = File(mangaDir.absolutePath, DEFAULT_COVER_NAME)
|
||||
}
|
||||
|
||||
// It might not exist at this point
|
||||
coverFile.parentFile?.mkdirs()
|
||||
inputStream.use { input ->
|
||||
coverFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a .nomedia file
|
||||
DiskUtil.createNoMediaFile(UniFile.fromFile(mangaDir), context)
|
||||
|
||||
manga.thumbnail_url = coverFile.absolutePath
|
||||
return coverFile
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "epub")
|
||||
|
@ -371,6 +371,6 @@ abstract class HttpSource : CatalogueSource {
|
||||
override fun getFilterList() = FilterList()
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36 Edg/88.0.705.63"
|
||||
const val DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.124 Safari/537.36 Edg/102.0.1245.44"
|
||||
}
|
||||
}
|
||||
|
@ -97,7 +97,7 @@ abstract class DialogController : Controller {
|
||||
/**
|
||||
* Dismiss the dialog and pop this controller
|
||||
*/
|
||||
private fun dismissDialog() {
|
||||
fun dismissDialog() {
|
||||
if (dismissed) {
|
||||
return
|
||||
}
|
||||
|
@ -20,6 +20,9 @@ interface ThemingDelegate {
|
||||
PreferenceValues.AppTheme.GREEN_APPLE -> {
|
||||
resIds += R.style.Theme_Tachiyomi_GreenApple
|
||||
}
|
||||
PreferenceValues.AppTheme.LAVENDER -> {
|
||||
resIds += R.style.Theme_Tachiyomi_Lavender
|
||||
}
|
||||
PreferenceValues.AppTheme.MIDNIGHT_DUSK -> {
|
||||
resIds += R.style.Theme_Tachiyomi_MidnightDusk
|
||||
}
|
||||
|
@ -129,7 +129,10 @@ class SearchController(
|
||||
}
|
||||
(targetController as? SearchController)?.copyManga(manga, newManga)
|
||||
}
|
||||
.setNeutralButton(android.R.string.cancel, null)
|
||||
.setNeutralButton(activity?.getString(R.string.action_show_manga)) { _, _ ->
|
||||
dismissDialog()
|
||||
router.pushController(MangaController(newManga).withFadeTransaction())
|
||||
}
|
||||
.create()
|
||||
}
|
||||
}
|
||||
|
@ -42,10 +42,10 @@ open class TriStateItem(val filter: Filter.TriState) : AbstractFlexibleItem<TriS
|
||||
else -> throw Exception("Unknown state")
|
||||
},
|
||||
)?.apply {
|
||||
val color = if (filter.state == Filter.TriState.STATE_INCLUDE) {
|
||||
view.context.getResourceColor(R.attr.colorAccent)
|
||||
} else {
|
||||
val color = if (filter.state == Filter.TriState.STATE_IGNORE) {
|
||||
view.context.getResourceColor(R.attr.colorOnBackground, 0.38f)
|
||||
} else {
|
||||
view.context.getResourceColor(R.attr.colorPrimary)
|
||||
}
|
||||
|
||||
setTint(color)
|
||||
|
@ -89,7 +89,7 @@ class DownloadHolder(private val view: View, val adapter: DownloadAdapter) :
|
||||
view.popupMenu(
|
||||
menuRes = R.menu.download_single,
|
||||
initMenu = {
|
||||
findItem(R.id.move_to_top).isVisible = bindingAdapterPosition != 0
|
||||
findItem(R.id.move_to_top).isVisible = bindingAdapterPosition > 1
|
||||
findItem(R.id.move_to_bottom).isVisible =
|
||||
bindingAdapterPosition != adapter.itemCount - 1
|
||||
},
|
||||
|
@ -8,7 +8,6 @@ import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.view.doOnAttach
|
||||
import androidx.core.view.isVisible
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
import com.bluelinelabs.conductor.ControllerChangeType
|
||||
@ -304,8 +303,10 @@ class LibraryController(
|
||||
onTabsSettingsChanged(firstLaunch = true)
|
||||
|
||||
// Delay the scroll position to allow the view to be properly measured.
|
||||
view.doOnAttach {
|
||||
(activity as? MainActivity)?.binding?.tabs?.setScrollPosition(binding.libraryPager.currentItem, 0f, true)
|
||||
view.post {
|
||||
if (isAttached) {
|
||||
(activity as? MainActivity)?.binding?.tabs?.setScrollPosition(binding.libraryPager.currentItem, 0f, true)
|
||||
}
|
||||
}
|
||||
|
||||
// Send the manga map to child fragments after the adapter is updated.
|
||||
|
@ -226,6 +226,11 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
presenter.saveProgress()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set menu visibility again on activity resume to apply immersive mode again if needed.
|
||||
* Helps with rotations.
|
||||
|
@ -4,7 +4,6 @@ import android.app.Application
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.History
|
||||
@ -22,6 +21,7 @@ import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.ui.reader.loader.ChapterLoader
|
||||
import eu.kanade.tachiyomi.ui.reader.loader.HttpPageLoader
|
||||
import eu.kanade.tachiyomi.ui.reader.model.InsertPage
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
@ -345,6 +345,14 @@ class ReaderPresenter(
|
||||
* that the user doesn't have to wait too long to continue reading.
|
||||
*/
|
||||
private fun preload(chapter: ReaderChapter) {
|
||||
if (chapter.pageLoader is HttpPageLoader) {
|
||||
val manga = manga ?: return
|
||||
val isDownloaded = downloadManager.isChapterDownloaded(chapter.chapter, manga)
|
||||
if (isDownloaded) {
|
||||
chapter.state = ReaderChapter.State.Wait
|
||||
}
|
||||
}
|
||||
|
||||
if (chapter.state != ReaderChapter.State.Wait && chapter.state !is ReaderChapter.State.Error) {
|
||||
return
|
||||
}
|
||||
@ -456,6 +464,10 @@ class ReaderPresenter(
|
||||
}
|
||||
}
|
||||
|
||||
fun saveProgress() {
|
||||
getCurrentChapter()?.let { onChapterChanged(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the activity to preload the given [chapter].
|
||||
*/
|
||||
@ -662,20 +674,22 @@ class ReaderPresenter(
|
||||
|
||||
Observable
|
||||
.fromCallable {
|
||||
if (manga.isLocal()) {
|
||||
val context = Injekt.get<Application>()
|
||||
LocalSource.updateCover(context, manga, stream())
|
||||
manga.updateCoverLastModified(db)
|
||||
R.string.cover_updated
|
||||
SetAsCoverResult.Success
|
||||
} else {
|
||||
if (manga.favorite) {
|
||||
coverCache.setCustomCoverToCache(manga, stream())
|
||||
stream().use {
|
||||
if (manga.isLocal()) {
|
||||
val context = Injekt.get<Application>()
|
||||
LocalSource.updateCover(context, manga, it)
|
||||
manga.updateCoverLastModified(db)
|
||||
coverCache.clearMemoryCache()
|
||||
SetAsCoverResult.Success
|
||||
} else {
|
||||
SetAsCoverResult.AddToLibraryFirst
|
||||
if (manga.favorite) {
|
||||
coverCache.setCustomCoverToCache(manga, it)
|
||||
manga.updateCoverLastModified(db)
|
||||
coverCache.clearMemoryCache()
|
||||
SetAsCoverResult.Success
|
||||
} else {
|
||||
SetAsCoverResult.AddToLibraryFirst
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,6 @@ class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: At
|
||||
is ChapterTransition.Prev -> bindPrevChapterTransition(transition)
|
||||
is ChapterTransition.Next -> bindNextChapterTransition(transition)
|
||||
}
|
||||
|
||||
missingChapterWarning(transition)
|
||||
}
|
||||
|
||||
|
@ -66,9 +66,14 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
|
||||
set(value) {
|
||||
field = value
|
||||
if (value) {
|
||||
awaitingIdleViewerChapters?.let {
|
||||
setChaptersInternal(it)
|
||||
awaitingIdleViewerChapters?.let { viewerChapters ->
|
||||
setChaptersInternal(viewerChapters)
|
||||
awaitingIdleViewerChapters = null
|
||||
if (viewerChapters.currChapter.pages?.size == 1) {
|
||||
adapter.nextTransition?.to?.let {
|
||||
activity.requestPreloadChapter(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ class WebtoonTransitionHolder(
|
||||
layout.orientation = LinearLayout.VERTICAL
|
||||
layout.gravity = Gravity.CENTER
|
||||
|
||||
val paddingVertical = 48.dpToPx
|
||||
val paddingVertical = 128.dpToPx
|
||||
val paddingHorizontal = 32.dpToPx
|
||||
layout.setPadding(paddingHorizontal, paddingVertical, paddingHorizontal, paddingVertical)
|
||||
|
||||
|
@ -103,6 +103,12 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
|
||||
activity.requestPreloadChapter(firstItem.to)
|
||||
}
|
||||
}
|
||||
|
||||
val lastIndex = layoutManager.findLastEndVisibleItemPosition()
|
||||
val lastItem = adapter.items.getOrNull(lastIndex)
|
||||
if (lastItem is ChapterTransition.Next && lastItem.to == null) {
|
||||
activity.showMenu()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
@ -216,9 +222,6 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
|
||||
if (toChapter != null) {
|
||||
logcat { "Request preload destination chapter because we're on the transition" }
|
||||
activity.requestPreloadChapter(toChapter)
|
||||
} else if (transition is ChapterTransition.Next) {
|
||||
// No more chapters, show menu because the user is probably going to close the reader
|
||||
activity.showMenu()
|
||||
}
|
||||
}
|
||||
|
||||
@ -245,7 +248,7 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
|
||||
logcat { "moveToPage" }
|
||||
val position = adapter.items.indexOf(page)
|
||||
if (position != -1) {
|
||||
recycler.scrollToPosition(position)
|
||||
layoutManager.scrollToPositionWithOffset(position, 0)
|
||||
if (layoutManager.findLastEndVisibleItemPosition() == -1) {
|
||||
onScrolled(pos = position)
|
||||
}
|
||||
|
@ -16,9 +16,13 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Target
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_360
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_ADGUARD
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_ALIDNS
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_DNSPOD
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_GOOGLE
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_QUAD101
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_QUAD9
|
||||
import eu.kanade.tachiyomi.ui.base.controller.openInBrowser
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
@ -48,6 +52,7 @@ import eu.kanade.tachiyomi.util.system.toast
|
||||
import logcat.LogPriority
|
||||
import rikka.sui.Sui
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||
|
||||
class SettingsAdvancedController : SettingsController() {
|
||||
@ -143,12 +148,6 @@ class SettingsAdvancedController : SettingsController() {
|
||||
titleRes = R.string.pref_auto_clear_chapter_cache
|
||||
defaultValue = false
|
||||
}
|
||||
preference {
|
||||
key = "pref_clear_webview_data"
|
||||
titleRes = R.string.pref_clear_webview_data
|
||||
|
||||
onClick { clearWebViewData() }
|
||||
}
|
||||
preference {
|
||||
key = "pref_clear_database"
|
||||
titleRes = R.string.pref_clear_database
|
||||
@ -172,6 +171,12 @@ class SettingsAdvancedController : SettingsController() {
|
||||
activity?.toast(R.string.cookies_cleared)
|
||||
}
|
||||
}
|
||||
preference {
|
||||
key = "pref_clear_webview_data"
|
||||
titleRes = R.string.pref_clear_webview_data
|
||||
|
||||
onClick { clearWebViewData() }
|
||||
}
|
||||
intListPreference {
|
||||
key = Keys.dohProvider
|
||||
titleRes = R.string.pref_dns_over_https
|
||||
@ -181,6 +186,10 @@ class SettingsAdvancedController : SettingsController() {
|
||||
"Google",
|
||||
"AdGuard",
|
||||
"Quad9",
|
||||
"AliDNS",
|
||||
"DNSPod",
|
||||
"360",
|
||||
"Quad 101",
|
||||
)
|
||||
entryValues = arrayOf(
|
||||
"-1",
|
||||
@ -188,6 +197,10 @@ class SettingsAdvancedController : SettingsController() {
|
||||
PREF_DOH_GOOGLE.toString(),
|
||||
PREF_DOH_ADGUARD.toString(),
|
||||
PREF_DOH_QUAD9.toString(),
|
||||
PREF_DOH_ALIDNS.toString(),
|
||||
PREF_DOH_DNSPOD.toString(),
|
||||
PREF_DOH_360.toString(),
|
||||
PREF_DOH_QUAD101.toString(),
|
||||
)
|
||||
defaultValue = "-1"
|
||||
summary = "%s"
|
||||
@ -301,6 +314,7 @@ class SettingsAdvancedController : SettingsController() {
|
||||
webview.clearHistory()
|
||||
webview.clearSslPreferences()
|
||||
WebStorage.getInstance().deleteAllData()
|
||||
activity?.applicationInfo?.dataDir?.let { File("$it/app_webview/").deleteRecursively() }
|
||||
activity?.toast(R.string.webview_data_deleted)
|
||||
} catch (e: Throwable) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
|
@ -125,20 +125,20 @@ class SettingsDownloadController : SettingsController() {
|
||||
titleRes = R.string.pref_category_auto_download
|
||||
|
||||
switchPreference {
|
||||
bindTo(preferences.downloadNew())
|
||||
bindTo(preferences.downloadNewChapter())
|
||||
titleRes = R.string.pref_download_new
|
||||
}
|
||||
preference {
|
||||
bindTo(preferences.downloadNewCategories())
|
||||
bindTo(preferences.downloadNewChapterCategories())
|
||||
titleRes = R.string.categories
|
||||
onClick {
|
||||
DownloadCategoriesDialog().showDialog(router)
|
||||
}
|
||||
|
||||
visibleIf(preferences.downloadNew()) { it }
|
||||
visibleIf(preferences.downloadNewChapter()) { it }
|
||||
|
||||
fun updateSummary() {
|
||||
val selectedCategories = preferences.downloadNewCategories().get()
|
||||
val selectedCategories = preferences.downloadNewChapterCategories().get()
|
||||
.mapNotNull { id -> categories.find { it.id == id.toInt() } }
|
||||
.sortedBy { it.order }
|
||||
val includedItemsText = if (selectedCategories.isEmpty()) {
|
||||
@ -147,7 +147,7 @@ class SettingsDownloadController : SettingsController() {
|
||||
selectedCategories.joinToString { it.name }
|
||||
}
|
||||
|
||||
val excludedCategories = preferences.downloadNewCategoriesExclude().get()
|
||||
val excludedCategories = preferences.downloadNewChapterCategoriesExclude().get()
|
||||
.mapNotNull { id -> categories.find { it.id == id.toInt() } }
|
||||
.sortedBy { it.order }
|
||||
val excludedItemsText = if (excludedCategories.isEmpty()) {
|
||||
@ -163,10 +163,10 @@ class SettingsDownloadController : SettingsController() {
|
||||
}
|
||||
}
|
||||
|
||||
preferences.downloadNewCategories().asFlow()
|
||||
preferences.downloadNewChapterCategories().asFlow()
|
||||
.onEach { updateSummary() }
|
||||
.launchIn(viewScope)
|
||||
preferences.downloadNewCategoriesExclude().asFlow()
|
||||
preferences.downloadNewChapterCategoriesExclude().asFlow()
|
||||
.onEach { updateSummary() }
|
||||
.launchIn(viewScope)
|
||||
}
|
||||
@ -254,8 +254,8 @@ class SettingsDownloadController : SettingsController() {
|
||||
var selected = categories
|
||||
.map {
|
||||
when (it.id.toString()) {
|
||||
in preferences.downloadNewCategories().get() -> QuadStateTextView.State.CHECKED.ordinal
|
||||
in preferences.downloadNewCategoriesExclude().get() -> QuadStateTextView.State.INVERSED.ordinal
|
||||
in preferences.downloadNewChapterCategories().get() -> QuadStateTextView.State.CHECKED.ordinal
|
||||
in preferences.downloadNewChapterCategoriesExclude().get() -> QuadStateTextView.State.INVERSED.ordinal
|
||||
else -> QuadStateTextView.State.UNCHECKED.ordinal
|
||||
}
|
||||
}
|
||||
@ -282,8 +282,8 @@ class SettingsDownloadController : SettingsController() {
|
||||
.map { categories[it].id.toString() }
|
||||
.toSet()
|
||||
|
||||
preferences.downloadNewCategories().set(included)
|
||||
preferences.downloadNewCategoriesExclude().set(excluded)
|
||||
preferences.downloadNewChapterCategories().set(included)
|
||||
preferences.downloadNewChapterCategoriesExclude().set(excluded)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create()
|
||||
|
@ -11,12 +11,7 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.data.preference.DEVICE_CHARGING
|
||||
import eu.kanade.tachiyomi.data.preference.DEVICE_ONLY_ON_WIFI
|
||||
import eu.kanade.tachiyomi.data.preference.MANGA_HAS_UNREAD
|
||||
import eu.kanade.tachiyomi.data.preference.MANGA_NON_COMPLETED
|
||||
import eu.kanade.tachiyomi.data.preference.MANGA_NON_READ
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.*
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.databinding.PrefLibraryColumnsBinding
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
@ -159,8 +154,8 @@ class SettingsLibraryController : SettingsController() {
|
||||
multiSelectListPreference {
|
||||
bindTo(preferences.libraryUpdateDeviceRestriction())
|
||||
titleRes = R.string.pref_library_update_restriction
|
||||
entriesRes = arrayOf(R.string.connected_to_wifi, R.string.charging)
|
||||
entryValues = arrayOf(DEVICE_ONLY_ON_WIFI, DEVICE_CHARGING)
|
||||
entriesRes = arrayOf(R.string.connected_to_wifi, R.string.network_not_metered, R.string.charging, R.string.battery_not_low)
|
||||
entryValues = arrayOf(DEVICE_ONLY_ON_WIFI, DEVICE_NETWORK_NOT_METERED, DEVICE_CHARGING, DEVICE_BATTERY_NOT_LOW)
|
||||
|
||||
visibleIf(preferences.libraryUpdateInterval()) { it > 0 }
|
||||
|
||||
@ -176,7 +171,9 @@ class SettingsLibraryController : SettingsController() {
|
||||
.map {
|
||||
when (it) {
|
||||
DEVICE_ONLY_ON_WIFI -> context.getString(R.string.connected_to_wifi)
|
||||
DEVICE_NETWORK_NOT_METERED -> context.getString(R.string.network_not_metered)
|
||||
DEVICE_CHARGING -> context.getString(R.string.charging)
|
||||
DEVICE_BATTERY_NOT_LOW -> context.getString(R.string.battery_not_low)
|
||||
else -> it
|
||||
}
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ class ClearDatabaseController :
|
||||
|
||||
adapter = FlexibleAdapter<ClearDatabaseSourceItem>(null, this, true)
|
||||
binding.recycler.adapter = adapter
|
||||
binding.recycler.layoutManager = LinearLayoutManager(activity)
|
||||
binding.recycler.layoutManager = LinearLayoutManager(activity!!)
|
||||
binding.recycler.setHasFixedSize(true)
|
||||
adapter?.fastScroller = binding.fastScroller
|
||||
recycler = binding.recycler
|
||||
|
@ -56,14 +56,14 @@ fun Manga.shouldDownloadNewChapters(db: DatabaseHelper, prefs: PreferencesHelper
|
||||
if (!favorite) return false
|
||||
|
||||
// Boolean to determine if user wants to automatically download new chapters.
|
||||
val downloadNew = prefs.downloadNew().get()
|
||||
if (!downloadNew) return false
|
||||
val downloadNewChapter = prefs.downloadNewChapter().get()
|
||||
if (!downloadNewChapter) return false
|
||||
|
||||
val categoriesToDownload = prefs.downloadNewCategories().get().map(String::toInt)
|
||||
val categoriesToExclude = prefs.downloadNewCategoriesExclude().get().map(String::toInt)
|
||||
val includedCategories = prefs.downloadNewChapterCategories().get().map { it.toInt() }
|
||||
val excludedCategories = prefs.downloadNewChapterCategoriesExclude().get().map { it.toInt() }
|
||||
|
||||
// Default: download from all categories
|
||||
if (categoriesToDownload.isEmpty() && categoriesToExclude.isEmpty()) return true
|
||||
// Default: Download from all categories
|
||||
if (includedCategories.isEmpty() && excludedCategories.isEmpty()) return true
|
||||
|
||||
// Get all categories, else default category (0)
|
||||
val categoriesForManga =
|
||||
@ -72,8 +72,11 @@ fun Manga.shouldDownloadNewChapters(db: DatabaseHelper, prefs: PreferencesHelper
|
||||
.takeUnless { it.isEmpty() } ?: listOf(0)
|
||||
|
||||
// In excluded category
|
||||
if (categoriesForManga.intersect(categoriesToExclude).isNotEmpty()) return false
|
||||
if (categoriesForManga.any { it in excludedCategories }) return false
|
||||
|
||||
// Included category not selected
|
||||
if (includedCategories.isEmpty()) return true
|
||||
|
||||
// In included category
|
||||
return categoriesForManga.intersect(categoriesToDownload).isNotEmpty()
|
||||
return categoriesForManga.any { it in includedCategories }
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.Date
|
||||
import java.util.TreeSet
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* Helper method for syncing the list of chapters from the source with the ones from the database.
|
||||
@ -59,6 +60,9 @@ fun syncChaptersWithSource(
|
||||
}
|
||||
}
|
||||
|
||||
var maxTimestamp = 0L // in previous chapters to add
|
||||
val rightNow = Date().time
|
||||
|
||||
for (sourceChapter in sourceChapters) {
|
||||
// This forces metadata update for the main viewable things in the chapter list.
|
||||
if (source is HttpSource) {
|
||||
@ -72,7 +76,9 @@ fun syncChaptersWithSource(
|
||||
// Add the chapter if not in db already, or update if the metadata changed.
|
||||
if (dbChapter == null) {
|
||||
if (sourceChapter.date_upload == 0L) {
|
||||
sourceChapter.date_upload = Date().time
|
||||
sourceChapter.date_upload = if (maxTimestamp == 0L) rightNow else maxTimestamp
|
||||
} else {
|
||||
maxTimestamp = max(maxTimestamp, sourceChapter.date_upload)
|
||||
}
|
||||
toAdd.add(sourceChapter)
|
||||
} else {
|
||||
@ -97,6 +103,7 @@ fun syncChaptersWithSource(
|
||||
return Pair(emptyList(), emptyList())
|
||||
}
|
||||
|
||||
// Keep it a List instead of a Set. See #6372.
|
||||
val readded = mutableListOf<Chapter>()
|
||||
|
||||
db.inTransaction {
|
||||
@ -154,6 +161,7 @@ fun syncChaptersWithSource(
|
||||
db.updateLastUpdated(manga).executeAsBlocking()
|
||||
}
|
||||
|
||||
@Suppress("ConvertArgumentToSet")
|
||||
return Pair(toAdd.subtract(readded).toList(), toDelete.subtract(readded).toList())
|
||||
}
|
||||
|
||||
|
@ -87,7 +87,11 @@ fun Context.copyToClipboard(label: String, content: String) {
|
||||
val clipboard = getSystemService<ClipboardManager>()!!
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText(label, content))
|
||||
|
||||
toast(getString(R.string.copied_to_clipboard, content.truncateCenter(50)))
|
||||
// Android 13 and higher shows a visual confirmation of copied contents
|
||||
// https://developer.android.com/about/versions/13/features/copy-paste
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||
toast(getString(R.string.copied_to_clipboard, content.truncateCenter(50)))
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
toast(R.string.clipboard_copy_error)
|
||||
|
@ -10,6 +10,7 @@ import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Build
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.core.graphics.alpha
|
||||
import androidx.core.graphics.applyCanvas
|
||||
import androidx.core.graphics.blue
|
||||
@ -56,6 +57,12 @@ object ImageUtil {
|
||||
return null
|
||||
}
|
||||
|
||||
fun getExtensionFromMimeType(mime: String?): String {
|
||||
return MimeTypeMap.getSingleton().getExtensionFromMimeType(mime)
|
||||
?: SUPPLEMENTARY_MIMETYPE_MAPPING[mime]
|
||||
?: "jpg"
|
||||
}
|
||||
|
||||
fun isAnimatedAndSupported(stream: InputStream): Boolean {
|
||||
try {
|
||||
val type = getImageType(stream) ?: return false
|
||||
@ -392,4 +399,10 @@ object ImageUtil {
|
||||
|
||||
private fun Int.isWhite(): Boolean =
|
||||
red + blue + green > 740
|
||||
|
||||
// Android doesn't include some mappings
|
||||
private val SUPPLEMENTARY_MIMETYPE_MAPPING = mapOf(
|
||||
// https://issuetracker.google.com/issues/182703810
|
||||
"image/jxl" to "jxl",
|
||||
)
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ open class ExtendedNavigationView @JvmOverloads constructor(
|
||||
* @param context any context.
|
||||
* @param resId the vector resource to load and tint
|
||||
*/
|
||||
fun tintVector(context: Context, resId: Int, @AttrRes colorAttrRes: Int = R.attr.colorAccent): Drawable {
|
||||
fun tintVector(context: Context, resId: Int, @AttrRes colorAttrRes: Int = R.attr.colorPrimary): Drawable {
|
||||
return AppCompatResources.getDrawable(context, resId)!!.apply {
|
||||
setTint(context.getResourceColor(if (enabled) colorAttrRes else R.attr.colorControlNormal))
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ class QuadStateTextView @JvmOverloads constructor(context: Context, attrs: Attri
|
||||
val tint = if (state == State.UNCHECKED) {
|
||||
context.getThemeColor(R.attr.colorControlNormal)
|
||||
} else {
|
||||
context.getThemeColor(R.attr.colorAccent)
|
||||
context.getThemeColor(R.attr.colorPrimary)
|
||||
}
|
||||
if (tint != 0) {
|
||||
TextViewCompat.setCompoundDrawableTintList(this, ColorStateList.valueOf(tint))
|
||||
|
5
app/src/main/res/color/slider_active_track.xml
Normal file
5
app/src/main/res/color/slider_active_track.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?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>
|
5
app/src/main/res/color/slider_inactive_track.xml
Normal file
5
app/src/main/res/color/slider_inactive_track.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:alpha="0.24" android:color="?attr/colorPrimary" android:state_enabled="true"/>
|
||||
<item android:alpha="@dimen/material_emphasis_disabled" android:color="?attr/colorOnSurface"/>
|
||||
</selector>
|
@ -1,8 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:width="36dp"
|
||||
android:height="36dp"
|
||||
android:drawable="@drawable/ic_tachi"
|
||||
android:gravity="center" />
|
||||
</layer-list>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:pathData="M50.61,39.254C50.65,40.029 50.691,41.036 50.719,41.513L50.773,42.371L44.96,42.331C41.772,42.303 39.049,42.235 38.926,42.166C38.695,42.058 38.682,42.248 38.682,44.944L38.682,47.831L38.995,47.75C39.893,47.491 43.53,47.395 53.796,47.395C64.064,47.395 67.698,47.491 68.611,47.75L68.911,47.831L68.911,44.944C68.911,42.248 68.897,42.058 68.678,42.166C68.542,42.235 65.819,42.303 62.634,42.331L56.819,42.371L56.873,41.513C56.9,41.036 56.941,40.029 56.981,39.254L57.037,37.864L50.555,37.864L50.61,39.254ZM44.645,49.329C43.23,49.861 42.086,50.377 42.086,50.459C42.086,50.555 42.276,51.044 42.509,51.563C43.597,53.973 45.312,59.937 45.79,62.945C45.87,63.53 46.008,64.008 46.089,64.008C46.171,64.008 47.505,63.572 49.072,63.041C51.1,62.333 51.889,62.006 51.889,61.856C51.889,61.42 49.467,53.551 48.322,50.282C47.805,48.839 47.586,48.349 47.41,48.362C47.287,48.362 46.049,48.798 44.645,49.329Z"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M59.406,49.397C58.957,52.257 57.282,57.785 55.484,62.333L54.545,64.688L37.456,64.688L37.456,70.136L70.544,70.136L70.544,64.688L65.493,64.688C61.503,64.688 60.454,64.648 60.509,64.511C60.55,64.43 60.876,63.708 61.23,62.918C61.571,62.128 62.252,60.426 62.728,59.12C63.641,56.614 65.643,50.364 65.643,49.997C65.643,49.819 65.016,49.614 62.756,49.057C61.163,48.662 59.788,48.349 59.706,48.349C59.624,48.349 59.488,48.825 59.406,49.397Z"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
||||
|
@ -95,9 +95,10 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/left_page_text"
|
||||
android:layout_width="32dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:minWidth="32dp"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:textSize="15sp"
|
||||
tools:text="1" />
|
||||
@ -116,9 +117,10 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/right_page_text"
|
||||
android:layout_width="32dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:minWidth="32dp"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:textSize="15sp"
|
||||
tools:text="15" />
|
||||
|
@ -11,7 +11,6 @@
|
||||
android:id="@+id/upper_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||
tools:text="Top" />
|
||||
|
||||
@ -19,7 +18,7 @@
|
||||
android:id="@+id/warning"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<ImageView
|
||||
@ -44,6 +43,7 @@
|
||||
android:id="@+id/lower_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||
tools:text="Bottom" />
|
||||
|
||||
|
37
app/src/main/res/values-night/color_lavender.xml
Normal file
37
app/src/main/res/values-night/color_lavender.xml
Normal file
@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Colors for Lavender theme
|
||||
~
|
||||
~ Color scheme by Osyx
|
||||
~
|
||||
~ Key colors:
|
||||
~ Primary #A177FF
|
||||
~ Secondary #A177FF
|
||||
~ Tertiary #5E25E1
|
||||
~ Neutral #111129
|
||||
-->
|
||||
<resources>
|
||||
<color name="lavender_primary">#A177FF</color>
|
||||
<color name="lavender_onPrimary">#111129</color>
|
||||
<color name="lavender_primaryContainer">#A177FF</color>
|
||||
<color name="lavender_onPrimaryContainer">#111129</color>
|
||||
<color name="lavender_secondary">#A177FF</color>
|
||||
<color name="lavender_onSecondary">#111129</color>
|
||||
<color name="lavender_secondaryContainer">#A177FF</color>
|
||||
<color name="lavender_onSecondaryContainer">#111129</color>
|
||||
<color name="lavender_tertiary">#5E25E1</color>
|
||||
<color name="lavender_onTertiary">#E8E8E8</color>
|
||||
<color name="lavender_tertiaryContainer">#111129</color>
|
||||
<color name="lavender_onTertiaryContainer">#DEE8FF</color>
|
||||
<color name="lavender_background">#111129</color>
|
||||
<color name="lavender_onBackground">#DEE8FF</color>
|
||||
<color name="lavender_surface">#111129</color>
|
||||
<color name="lavender_onSurface">#DEE8FF</color>
|
||||
<color name="lavender_surfaceVariant">#2CB6B6B6</color>
|
||||
<color name="lavender_onSurfaceVariant">#E8E8E8</color>
|
||||
<color name="lavender_outline">#A8905FFF</color>
|
||||
<color name="lavender_inverseOnSurface">#DEE8FF</color>
|
||||
<color name="lavender_inverseSurface">#221247</color>
|
||||
<color name="lavender_primaryInverse">#A177FF</color>
|
||||
<color name="lavender_elevationOverlay">@color/lavender_primary</color>
|
||||
</resources>
|
36
app/src/main/res/values/color_lavender.xml
Normal file
36
app/src/main/res/values/color_lavender.xml
Normal file
@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Colors for Lavender theme
|
||||
~ Original color scheme by CrepeTF
|
||||
~
|
||||
~ Key colors:
|
||||
~ Primary #7B46AF
|
||||
~ Secondary #7B46AF
|
||||
~ Tertiary #EDE2FF
|
||||
~ Neutral #EDE2FF
|
||||
-->
|
||||
<resources>
|
||||
<color name="lavender_primary">#7B46AF</color>
|
||||
<color name="lavender_onPrimary">#EDE2FF</color>
|
||||
<color name="lavender_primaryContainer">#7B46AF</color>
|
||||
<color name="lavender_onPrimaryContainer">#EDE2FF</color>
|
||||
<color name="lavender_secondary">#7B46AF</color>
|
||||
<color name="lavender_onSecondary">#EDE2FF</color>
|
||||
<color name="lavender_secondaryContainer">#7B46AF</color>
|
||||
<color name="lavender_onSecondaryContainer">#EDE2FF</color>
|
||||
<color name="lavender_tertiary">#EDE2FF</color>
|
||||
<color name="lavender_onTertiary">#7B46AF</color>
|
||||
<color name="lavender_tertiaryContainer">#EDE2FF</color>
|
||||
<color name="lavender_onTertiaryContainer">#7B46AF</color>
|
||||
<color name="lavender_background">#EDE2FF</color>
|
||||
<color name="lavender_onBackground">#1B1B22</color>
|
||||
<color name="lavender_surface">#EDE2FF</color>
|
||||
<color name="lavender_onSurface">#1B1B22</color>
|
||||
<color name="lavender_surfaceVariant">#B9B0CC</color>
|
||||
<color name="lavender_onSurfaceVariant">#D849454E</color>
|
||||
<color name="lavender_outline">#7B46AF</color>
|
||||
<color name="lavender_inverseOnSurface">#F3EFF4</color>
|
||||
<color name="lavender_inverseSurface">#313033</color>
|
||||
<color name="lavender_primaryInverse">#D6BAFF</color>
|
||||
<color name="lavender_elevationOverlay">@color/lavender_primary</color>
|
||||
</resources>
|
@ -157,6 +157,7 @@
|
||||
<string name="pref_app_theme">App theme</string>
|
||||
<string name="theme_monet">Dynamic</string>
|
||||
<string name="theme_greenapple">Green Apple</string>
|
||||
<string name="theme_lavender">Lavender</string>
|
||||
<string name="theme_midnightdusk">Midnight Dusk</string>
|
||||
<string name="theme_strawberrydaiquiri">Strawberry Daiquiri</string>
|
||||
<string name="theme_tako">Tako</string>
|
||||
@ -222,7 +223,9 @@
|
||||
<string name="update_weekly">Weekly</string>
|
||||
<string name="pref_library_update_restriction">Automatic updates device restrictions</string>
|
||||
<string name="connected_to_wifi">Only on Wi-Fi</string>
|
||||
<string name="network_not_metered">Only on unmetered Network</string>
|
||||
<string name="charging">Charging</string>
|
||||
<string name="battery_not_low">Battery not low</string>
|
||||
<string name="restrictions">Restrictions: %s</string>
|
||||
|
||||
<string name="pref_library_update_manga_restriction">Skip updating titles</string>
|
||||
@ -283,8 +286,8 @@
|
||||
|
||||
<!-- Reader section -->
|
||||
<string name="pref_fullscreen">Fullscreen</string>
|
||||
<string name="pref_show_navigation_mode">Show navigation layout overlay</string>
|
||||
<string name="pref_show_navigation_mode_summary">Show tap zones when reader is opened</string>
|
||||
<string name="pref_show_navigation_mode">Show tap zones overlay</string>
|
||||
<string name="pref_show_navigation_mode_summary">Briefly show when reader is opened</string>
|
||||
<string name="pref_dual_page_split">Dual page split</string>
|
||||
<string name="pref_dual_page_invert">Invert dual page split placement</string>
|
||||
<string name="pref_dual_page_invert_summary">If the placement of the dual page split doesn\'t match reading direction</string>
|
||||
@ -295,7 +298,7 @@
|
||||
<string name="pref_show_reading_mode">Show reading mode</string>
|
||||
<string name="pref_show_reading_mode_summary">Briefly show current mode when reader is opened</string>
|
||||
<string name="pref_true_color">32-bit color</string>
|
||||
<string name="pref_true_color_summary">Reduces banding, but impacts performance</string>
|
||||
<string name="pref_true_color_summary">Reduces banding, but may impact performance</string>
|
||||
<string name="pref_crop_borders">Crop borders</string>
|
||||
<string name="on">On</string>
|
||||
<string name="off">Off</string>
|
||||
@ -315,7 +318,7 @@
|
||||
<string name="pref_reader_navigation">Navigation</string>
|
||||
<string name="pref_read_with_volume_keys">Volume keys</string>
|
||||
<string name="pref_read_with_volume_keys_inverted">Invert volume keys</string>
|
||||
<string name="pref_read_with_tapping_inverted">Invert tapping</string>
|
||||
<string name="pref_read_with_tapping_inverted">Invert tap zones</string>
|
||||
<string name="tapping_inverted_none">None</string>
|
||||
<string name="tapping_inverted_horizontal">Horizontal</string>
|
||||
<string name="tapping_inverted_vertical">Vertical</string>
|
||||
@ -345,7 +348,7 @@
|
||||
<string name="webtoon_viewer">Webtoon</string>
|
||||
<string name="vertical_plus_viewer">Continuous vertical</string>
|
||||
<string name="pager_viewer">Paged</string>
|
||||
<string name="pref_viewer_nav">Navigation layout</string>
|
||||
<string name="pref_viewer_nav">Tap zones</string>
|
||||
<string name="pref_image_scale_type">Scale type</string>
|
||||
<string name="scale_type_fit_screen">Fit screen</string>
|
||||
<string name="scale_type_stretch">Stretch</string>
|
||||
@ -720,12 +723,12 @@
|
||||
<!-- Downloads activity and service -->
|
||||
<string name="download_queue_error">Couldn\'t download chapters. You can try again in the downloads section</string>
|
||||
<string name="download_insufficient_space">Couldn\'t download chapters due to low storage space</string>
|
||||
<string name="download_queue_size_warning">Warning: large bulk downloads may lead to sources becoming slower and/or blocking Tachiyomi</string>
|
||||
<string name="download_queue_size_warning">Warning: large bulk downloads may lead to sources becoming slower and/or blocking Tachiyomi. Tap to learn more.</string>
|
||||
|
||||
<!-- Library update service notifications -->
|
||||
<string name="notification_check_updates">Checking for new chapters</string>
|
||||
<string name="notification_updating">Updating library… (%1$d/%2$d)</string>
|
||||
<string name="notification_size_warning">Large updates harm sources and may lead to slower updates and also increased battery usage</string>
|
||||
<string name="notification_size_warning">Large updates harm sources and may lead to slower updates and also increased battery usage. Tap to learn more.</string>
|
||||
<string name="notification_new_chapters">New chapters found</string>
|
||||
<plurals name="notification_new_chapters_summary">
|
||||
<item quantity="one">For %d title</item>
|
||||
|
@ -192,6 +192,8 @@
|
||||
<style name="Widget.Tachiyomi.Slider" parent="Widget.Material3.Slider">
|
||||
<item name="labelBehavior">gone</item>
|
||||
<item name="tickVisible">false</item>
|
||||
<item name="trackColorInactive">@color/slider_inactive_track</item>
|
||||
<item name="trackColorActive">@color/slider_active_track</item>
|
||||
</style>
|
||||
|
||||
|
||||
|
@ -121,6 +121,34 @@
|
||||
<item name="colorPrimaryInverse">@color/greenapple_primaryInverse</item>
|
||||
</style>
|
||||
|
||||
<!--== Lavender Theme ==-->
|
||||
<style name="Theme.Tachiyomi.Lavender">
|
||||
<!-- Theme Colors -->
|
||||
<item name="colorPrimary">@color/lavender_primary</item>
|
||||
<item name="colorOnPrimary">@color/lavender_onPrimary</item>
|
||||
<item name="colorPrimaryContainer">@color/lavender_primaryContainer</item>
|
||||
<item name="colorOnPrimaryContainer">@color/lavender_onPrimaryContainer</item>
|
||||
<item name="colorSecondary">@color/lavender_secondary</item>
|
||||
<item name="colorOnSecondary">@color/lavender_onSecondary</item>
|
||||
<item name="colorSecondaryContainer">@color/lavender_secondaryContainer</item>
|
||||
<item name="colorOnSecondaryContainer">@color/lavender_onSecondaryContainer</item>
|
||||
<item name="colorTertiary">@color/lavender_tertiary</item>
|
||||
<item name="colorOnTertiary">@color/lavender_onTertiary</item>
|
||||
<item name="colorTertiaryContainer">@color/lavender_tertiaryContainer</item>
|
||||
<item name="colorOnTertiaryContainer">@color/lavender_onTertiaryContainer</item>
|
||||
<item name="android:colorBackground">@color/lavender_background</item>
|
||||
<item name="colorOnBackground">@color/lavender_onBackground</item>
|
||||
<item name="colorSurface">@color/lavender_surface</item>
|
||||
<item name="colorOnSurface">@color/lavender_onSurface</item>
|
||||
<item name="colorSurfaceVariant">@color/lavender_surfaceVariant</item>
|
||||
<item name="colorOnSurfaceVariant">@color/lavender_onSurfaceVariant</item>
|
||||
<item name="colorOutline">@color/lavender_outline</item>
|
||||
<item name="colorOnSurfaceInverse">@color/lavender_inverseOnSurface</item>
|
||||
<item name="colorSurfaceInverse">@color/lavender_inverseSurface</item>
|
||||
<item name="colorPrimaryInverse">@color/lavender_primaryInverse</item>
|
||||
<item name="elevationOverlayColor">@color/lavender_elevationOverlay</item>
|
||||
</style>
|
||||
|
||||
<!--== Midnight Dusk Theme ==-->
|
||||
<style name="Theme.Tachiyomi.MidnightDusk">
|
||||
<!-- Theme Colors -->
|
||||
|
@ -1,5 +1,5 @@
|
||||
object AndroidConfig {
|
||||
const val compileSdk = 31
|
||||
const val compileSdk = 32
|
||||
const val minSdk = 23
|
||||
const val targetSdk = 29
|
||||
const val ndk = "22.1.7171670"
|
||||
|
@ -1,16 +1,16 @@
|
||||
[versions]
|
||||
agp_version = "7.1.3"
|
||||
lifecycle_version = "2.5.0-alpha06"
|
||||
lifecycle_version = "2.5.0"
|
||||
|
||||
[libraries]
|
||||
annotation = "androidx.annotation:annotation:1.4.0-alpha02"
|
||||
appcompat = "androidx.appcompat:appcompat:1.4.1"
|
||||
annotation = "androidx.annotation:annotation:1.4.0"
|
||||
appcompat = "androidx.appcompat:appcompat:1.4.2"
|
||||
biometricktx = "androidx.biometric:biometric-ktx:1.2.0-alpha04"
|
||||
constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.3"
|
||||
constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.4"
|
||||
coordinatorlayout = "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
|
||||
corektx = "androidx.core:core-ktx:1.8.0-alpha07"
|
||||
corektx = "androidx.core:core-ktx:1.8.0"
|
||||
splashscreen = "androidx.core:core-splashscreen:1.0.0-alpha02"
|
||||
recyclerview = "androidx.recyclerview:recyclerview:1.3.0-alpha02"
|
||||
recyclerview = "androidx.recyclerview:recyclerview:1.3.0-beta01"
|
||||
swiperefreshlayout = "androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01"
|
||||
viewpager = "androidx.viewpager:viewpager:1.1.0-alpha01"
|
||||
|
||||
@ -26,5 +26,5 @@ lifecycle = ["lifecycle-common", "lifecycle-process", "lifecycle-runtimektx"]
|
||||
workmanager = ["work-runtime", "guava"]
|
||||
|
||||
[plugins]
|
||||
application = { id = "com.android.application", version.ref="agp_version"}
|
||||
library = { id = "com.android.library", version.ref="agp_version"}
|
||||
application = { id = "com.android.application", version.ref = "agp_version" }
|
||||
library = { id = "com.android.library", version.ref = "agp_version" }
|
@ -1,9 +1,9 @@
|
||||
[versions]
|
||||
aboutlib_version = "8.9.4"
|
||||
okhttp_version = "4.9.3"
|
||||
okhttp_version = "4.10.0"
|
||||
nucleus_version = "3.0.0"
|
||||
coil_version = "2.0.0-rc03"
|
||||
conductor_version = "3.1.2"
|
||||
conductor_version = "3.1.5"
|
||||
flowbinding_version = "1.2.0"
|
||||
shizuku_version = "12.1.0"
|
||||
robolectric_version = "3.1.4"
|
||||
@ -22,7 +22,7 @@ flowreactivenetwork = "ru.beryukhov:flowreactivenetwork:1.0.4"
|
||||
okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp_version" }
|
||||
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp_version" }
|
||||
okhttp-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "okhttp_version" }
|
||||
okio = "com.squareup.okio:okio:3.0.0"
|
||||
okio = "com.squareup.okio:okio:3.2.0"
|
||||
|
||||
conscrypt-android = "org.conscrypt:conscrypt-android:2.5.2"
|
||||
|
||||
@ -34,13 +34,13 @@ jsoup = "org.jsoup:jsoup:1.14.3"
|
||||
|
||||
disklrucache = "com.jakewharton:disklrucache:2.0.2"
|
||||
unifile = "com.github.tachiyomiorg:unifile:17bec43"
|
||||
junrar = "com.github.junrar:junrar:7.5.0"
|
||||
junrar = "com.github.junrar:junrar:7.5.2"
|
||||
|
||||
sqlitektx = "androidx.sqlite:sqlite-ktx:2.3.0-alpha02"
|
||||
sqlite-android = "com.github.requery:sqlite-android:3.36.0"
|
||||
|
||||
preferencektx = "androidx.preference:preference-ktx:1.2.0"
|
||||
flowpreferences = "com.fredporciuncula:flow-preferences:1.6.0"
|
||||
flowpreferences = "com.fredporciuncula:flow-preferences:1.7.0"
|
||||
|
||||
nucleus-core = { module = "info.android15.nucleus:nucleus", version.ref = "nucleus_version" }
|
||||
nucleus-supportv7 = { module = "info.android15.nucleus:nucleus-support-v7", version.ref = "nucleus_version" }
|
||||
@ -78,8 +78,8 @@ flowbinding-viewpager = { module = "io.github.reactivecircus.flowbinding:flowbin
|
||||
|
||||
logcat = "com.squareup.logcat:logcat:0.1"
|
||||
|
||||
acra-http = "ch.acra:acra-http:5.9.3"
|
||||
firebase-analytics = "com.google.firebase:firebase-analytics-ktx:20.0.2"
|
||||
acra-http = "ch.acra:acra-http:5.9.5"
|
||||
firebase-analytics = "com.google.firebase:firebase-analytics-ktx:21.0.0"
|
||||
|
||||
aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutlib_version" }
|
||||
aboutlibraries-gradle = { module = "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin", version.ref = "aboutlib_version" }
|
||||
|
Reference in New Issue
Block a user