mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-28 17:19:00 +03:00
Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
257456016b
29 changed files with 642 additions and 173 deletions
|
@ -134,7 +134,7 @@ dependencies {
|
|||
|
||||
implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN))
|
||||
|
||||
val coroutinesVersion = "1.4.3"
|
||||
val coroutinesVersion = "1.5.1"
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
|
||||
|
||||
|
@ -164,7 +164,7 @@ dependencies {
|
|||
implementation("androidx.work:work-runtime-ktx:2.6.0-beta01")
|
||||
|
||||
// UI library
|
||||
implementation("com.google.android.material:material:1.4.0")
|
||||
implementation("com.google.android.material:material:1.5.0-alpha01")
|
||||
|
||||
"standardImplementation"("com.google.firebase:firebase-core:19.0.0")
|
||||
|
||||
|
@ -185,7 +185,7 @@ dependencies {
|
|||
implementation("org.conscrypt:conscrypt-android:2.5.2")
|
||||
|
||||
// JSON
|
||||
val kotlinSerializationVersion = "1.2.0"
|
||||
val kotlinSerializationVersion = "1.2.2"
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
|
||||
implementation("com.google.code.gson:gson:2.8.7")
|
||||
|
|
|
@ -26,6 +26,7 @@ import coil.decode.ImageDecoderDecoder
|
|||
import eu.kanade.tachiyomi.data.coil.AnimeCoverFetcher
|
||||
import eu.kanade.tachiyomi.data.coil.ByteBufferFetcher
|
||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
||||
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
|
@ -111,6 +112,7 @@ open class App : Application(), LifecycleObserver, ImageLoaderFactory {
|
|||
override fun newImageLoader(): ImageLoader {
|
||||
return ImageLoader.Builder(this).apply {
|
||||
componentRegistry {
|
||||
add(TachiyomiImageDecoder(this@App.resources))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
add(ImageDecoderDecoder(this@App))
|
||||
} else {
|
||||
|
|
|
@ -29,7 +29,6 @@ class LocalAnimeSource(private val context: Context) : AnimeCatalogueSource {
|
|||
const val ID = 0L
|
||||
const val HELP_URL = "https://tachiyomi.org/help/guides/local-anime/"
|
||||
|
||||
private const val COVER_NAME = "cover.jpg"
|
||||
private val SUPPORTED_FILE_TYPES = setOf("mp4", "m3u8", "mkv")
|
||||
|
||||
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
|
||||
|
@ -40,18 +39,29 @@ class LocalAnimeSource(private val context: Context) : AnimeCatalogueSource {
|
|||
input.close()
|
||||
return null
|
||||
}
|
||||
val cover = File("${dir.absolutePath}/${anime.url}", COVER_NAME)
|
||||
val cover = getCoverFile(File("${dir.absolutePath}/${anime.url}"))
|
||||
|
||||
// It might not exist if using the external SD card
|
||||
cover.parentFile?.mkdirs()
|
||||
input.use {
|
||||
cover.outputStream().use {
|
||||
input.copyTo(it)
|
||||
if (cover != null && cover.exists()) {
|
||||
// It might not exist if using the external SD card
|
||||
cover.parentFile?.mkdirs()
|
||||
input.use {
|
||||
cover.outputStream().use {
|
||||
input.copyTo(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
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 + "localanime"
|
||||
return DiskUtil.getExternalStorages(context).map { File(it.absolutePath, c) }
|
||||
|
@ -105,8 +115,8 @@ class LocalAnimeSource(private val context: Context) : AnimeCatalogueSource {
|
|||
|
||||
// Try to find the cover
|
||||
for (dir in baseDirs) {
|
||||
val cover = File("${dir.absolutePath}/$url", COVER_NAME)
|
||||
if (cover.exists()) {
|
||||
val cover = getCoverFile(File("${dir.absolutePath}/$url"))
|
||||
if (cover != null && cover.exists()) {
|
||||
thumbnail_url = cover.absolutePath
|
||||
break
|
||||
}
|
||||
|
|
|
@ -64,22 +64,25 @@ class AnimelibUpdateNotifier(private val context: Context) {
|
|||
/**
|
||||
* Shows the notification containing the currently updating anime and the progress.
|
||||
*
|
||||
* @param anime the anime that's being updated.
|
||||
* @param anime the anime that are being updated.
|
||||
* @param current the current progress.
|
||||
* @param total the total progress.
|
||||
*/
|
||||
fun showProgressNotification(anime: Anime, current: Int, total: Int) {
|
||||
val title = if (preferences.hideNotificationContent()) {
|
||||
context.getString(R.string.notification_check_updates)
|
||||
fun showProgressNotification(anime: List<Anime>, current: Int, total: Int) {
|
||||
if (preferences.hideNotificationContent()) {
|
||||
progressNotificationBuilder
|
||||
.setContentTitle(context.getString(R.string.notification_check_updates))
|
||||
.setContentText("($current/$total)")
|
||||
} else {
|
||||
anime.title
|
||||
val updatingText = anime.joinToString("\n") { it.title }
|
||||
progressNotificationBuilder
|
||||
.setContentTitle(context.getString(R.string.notification_updating, current, total))
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(updatingText))
|
||||
}
|
||||
|
||||
context.notificationManager.notify(
|
||||
Notifications.ID_LIBRARY_PROGRESS,
|
||||
progressNotificationBuilder
|
||||
.setContentTitle(title.chop(40))
|
||||
.setContentText("($current/$total)")
|
||||
.setProgress(total, current, false)
|
||||
.build()
|
||||
)
|
||||
|
|
|
@ -30,6 +30,7 @@ import eu.kanade.tachiyomi.data.track.UnattendedTrackService
|
|||
import eu.kanade.tachiyomi.util.episode.NoEpisodesException
|
||||
import eu.kanade.tachiyomi.util.episode.syncEpisodesWithSource
|
||||
import eu.kanade.tachiyomi.util.episode.syncEpisodesWithTrackServiceTwoWay
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import eu.kanade.tachiyomi.util.prepUpdateCover
|
||||
import eu.kanade.tachiyomi.util.shouldDownloadNewEpisodes
|
||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
|
@ -47,10 +48,14 @@ import kotlinx.coroutines.awaitAll
|
|||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
/**
|
||||
|
@ -270,50 +275,76 @@ class AnimelibUpdateService(
|
|||
* @return an observable delivering the progress of each update.
|
||||
*/
|
||||
suspend fun updateEpisodeList() {
|
||||
val semaphore = Semaphore(5)
|
||||
val progressCount = AtomicInteger(0)
|
||||
val newUpdates = mutableListOf<Pair<AnimelibAnime, Array<Episode>>>()
|
||||
val failedUpdates = mutableListOf<Pair<Anime, String?>>()
|
||||
var hasDownloads = false
|
||||
val currentlyUpdatingAnime = CopyOnWriteArrayList<AnimelibAnime>()
|
||||
val newUpdates = CopyOnWriteArrayList<Pair<AnimelibAnime, Array<Episode>>>()
|
||||
val failedUpdates = CopyOnWriteArrayList<Pair<Anime, String?>>()
|
||||
val hasDownloads = AtomicBoolean(false)
|
||||
val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
|
||||
|
||||
animeToUpdate.forEach { anime ->
|
||||
if (updateJob?.isActive != true) {
|
||||
return
|
||||
}
|
||||
withIOContext {
|
||||
animeToUpdate.groupBy { it.source }
|
||||
.values
|
||||
.map { animeInSource ->
|
||||
async {
|
||||
semaphore.withPermit {
|
||||
animeInSource.forEach { anime ->
|
||||
if (updateJob?.isActive != true) {
|
||||
return@async
|
||||
}
|
||||
|
||||
notifier.showProgressNotification(anime, progressCount.andIncrement, animeToUpdate.size)
|
||||
currentlyUpdatingAnime.add(anime)
|
||||
progressCount.andIncrement
|
||||
notifier.showProgressNotification(
|
||||
currentlyUpdatingAnime,
|
||||
progressCount.get(),
|
||||
animeToUpdate.size
|
||||
)
|
||||
|
||||
try {
|
||||
val (newEpisodes, _) = updateAnime(anime)
|
||||
try {
|
||||
val (newEpisodes, _) = updateAnime(anime)
|
||||
|
||||
if (newEpisodes.isNotEmpty()) {
|
||||
if (anime.shouldDownloadNewEpisodes(db, preferences)) {
|
||||
downloadEpisodes(anime, newEpisodes)
|
||||
hasDownloads = true
|
||||
if (newEpisodes.isNotEmpty()) {
|
||||
if (anime.shouldDownloadNewEpisodes(db, preferences)) {
|
||||
downloadEpisodes(anime, newEpisodes)
|
||||
hasDownloads.set(true)
|
||||
}
|
||||
|
||||
// Convert to the manga that contains new chapters
|
||||
newUpdates.add(anime to newEpisodes.sortedByDescending { ep -> ep.source_order }.toTypedArray())
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
val errorMessage = if (e is NoEpisodesException) {
|
||||
getString(R.string.no_chapters_error)
|
||||
} else {
|
||||
e.message
|
||||
}
|
||||
failedUpdates.add(anime to errorMessage)
|
||||
}
|
||||
|
||||
if (preferences.autoUpdateTrackers()) {
|
||||
updateTrackings(anime, loggedServices)
|
||||
}
|
||||
|
||||
currentlyUpdatingAnime.remove(anime)
|
||||
notifier.showProgressNotification(
|
||||
currentlyUpdatingAnime,
|
||||
progressCount.get(),
|
||||
animeToUpdate.size
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to the anime that contains new chapters
|
||||
newUpdates.add(anime to newEpisodes.sortedByDescending { ep -> ep.source_order }.toTypedArray())
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
val errorMessage = if (e is NoEpisodesException) {
|
||||
getString(R.string.no_chapters_error)
|
||||
} else {
|
||||
e.message
|
||||
}
|
||||
failedUpdates.add(anime to errorMessage)
|
||||
}
|
||||
|
||||
if (preferences.autoUpdateTrackers()) {
|
||||
updateTrackings(anime, loggedServices)
|
||||
}
|
||||
.awaitAll()
|
||||
}
|
||||
|
||||
notifier.cancelProgressNotification()
|
||||
|
||||
if (newUpdates.isNotEmpty()) {
|
||||
notifier.showUpdateNotifications(newUpdates)
|
||||
if (hasDownloads) {
|
||||
if (hasDownloads.get()) {
|
||||
AnimeDownloadService.start(this)
|
||||
}
|
||||
}
|
||||
|
@ -369,30 +400,58 @@ class AnimelibUpdateService(
|
|||
}
|
||||
|
||||
private suspend fun updateCovers() {
|
||||
var progressCount = 0
|
||||
val semaphore = Semaphore(5)
|
||||
val progressCount = AtomicInteger(0)
|
||||
val currentlyUpdatingAnime = CopyOnWriteArrayList<AnimelibAnime>()
|
||||
|
||||
animeToUpdate.forEach { anime ->
|
||||
if (updateJob?.isActive != true) {
|
||||
return
|
||||
}
|
||||
withIOContext {
|
||||
animeToUpdate.groupBy { it.source }
|
||||
.values
|
||||
.map { animeInSource ->
|
||||
async {
|
||||
semaphore.withPermit {
|
||||
animeInSource.forEach { anime ->
|
||||
if (updateJob?.isActive != true) {
|
||||
return@async
|
||||
}
|
||||
|
||||
notifier.showProgressNotification(anime, progressCount++, animeToUpdate.size)
|
||||
currentlyUpdatingAnime.add(anime)
|
||||
progressCount.andIncrement
|
||||
notifier.showProgressNotification(
|
||||
currentlyUpdatingAnime,
|
||||
progressCount.get(),
|
||||
animeToUpdate.size
|
||||
)
|
||||
|
||||
sourceManager.get(anime.source)?.let { source ->
|
||||
try {
|
||||
val networkAnime = source.getAnimeDetails(anime.toAnimeInfo())
|
||||
val sAnime = networkAnime.toSAnime()
|
||||
anime.prepUpdateCover(coverCache, sAnime, true)
|
||||
sAnime.thumbnail_url?.let {
|
||||
anime.thumbnail_url = it
|
||||
db.insertAnime(anime).executeAsBlocking()
|
||||
sourceManager.get(anime.source)?.let { source ->
|
||||
try {
|
||||
val networkAnime =
|
||||
source.getAnimeDetails(anime.toAnimeInfo())
|
||||
val sAnime = networkAnime.toSAnime()
|
||||
anime.prepUpdateCover(coverCache, sAnime, true)
|
||||
sAnime.thumbnail_url?.let {
|
||||
anime.thumbnail_url = it
|
||||
db.insertAnime(anime).executeAsBlocking()
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
// Ignore errors and continue
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
|
||||
currentlyUpdatingAnime.remove(anime)
|
||||
notifier.showProgressNotification(
|
||||
currentlyUpdatingAnime,
|
||||
progressCount.get(),
|
||||
animeToUpdate.size
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
// Ignore errors and continue
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
}
|
||||
|
||||
coverCache.clearMemoryCache()
|
||||
notifier.cancelProgressNotification()
|
||||
}
|
||||
|
@ -410,8 +469,7 @@ class AnimelibUpdateService(
|
|||
return
|
||||
}
|
||||
|
||||
// Notify anime that will update.
|
||||
notifier.showProgressNotification(anime, progressCount++, animeToUpdate.size)
|
||||
notifier.showProgressNotification(listOf(anime), progressCount++, animeToUpdate.size)
|
||||
|
||||
// Update the tracking details.
|
||||
updateTrackings(anime, loggedServices)
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
package eu.kanade.tachiyomi.data.coil
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.os.Build
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import coil.bitmap.BitmapPool
|
||||
import coil.decode.DecodeResult
|
||||
import coil.decode.Decoder
|
||||
import coil.decode.Options
|
||||
import coil.size.Size
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import okio.BufferedSource
|
||||
import tachiyomi.decoder.ImageDecoder
|
||||
|
||||
/**
|
||||
* A [Decoder] that uses built-in [ImageDecoder] to decode images that is not supported by the system.
|
||||
*/
|
||||
class TachiyomiImageDecoder(private val resources: Resources) : Decoder {
|
||||
|
||||
override fun handles(source: BufferedSource, mimeType: String?): Boolean {
|
||||
val type = source.peek().inputStream().use {
|
||||
ImageUtil.findImageType(it)
|
||||
}
|
||||
return when (type) {
|
||||
ImageUtil.ImageType.AVIF, ImageUtil.ImageType.JXL -> true
|
||||
ImageUtil.ImageType.HEIF -> Build.VERSION.SDK_INT < Build.VERSION_CODES.O
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun decode(
|
||||
pool: BitmapPool,
|
||||
source: BufferedSource,
|
||||
size: Size,
|
||||
options: Options
|
||||
): DecodeResult {
|
||||
val decoder = source.use {
|
||||
ImageDecoder.newInstance(it.inputStream())
|
||||
}
|
||||
|
||||
check(decoder != null && decoder.width > 0 && decoder.height > 0) { "Failed to initialize decoder." }
|
||||
|
||||
val bitmap = decoder.decode(rgb565 = options.allowRgb565)
|
||||
decoder.recycle()
|
||||
|
||||
check(bitmap != null) { "Failed to decode image." }
|
||||
|
||||
return DecodeResult(
|
||||
drawable = bitmap.toDrawable(resources),
|
||||
isSampled = false
|
||||
)
|
||||
}
|
||||
}
|
|
@ -64,22 +64,25 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||
/**
|
||||
* Shows the notification containing the currently updating manga and the progress.
|
||||
*
|
||||
* @param manga the manga that's being updated.
|
||||
* @param manga the manga that are being updated.
|
||||
* @param current the current progress.
|
||||
* @param total the total progress.
|
||||
*/
|
||||
fun showProgressNotification(manga: Manga, current: Int, total: Int) {
|
||||
val title = if (preferences.hideNotificationContent()) {
|
||||
context.getString(R.string.notification_check_updates)
|
||||
fun showProgressNotification(manga: List<Manga>, current: Int, total: Int) {
|
||||
if (preferences.hideNotificationContent()) {
|
||||
progressNotificationBuilder
|
||||
.setContentTitle(context.getString(R.string.notification_check_updates))
|
||||
.setContentText("($current/$total)")
|
||||
} else {
|
||||
manga.title
|
||||
val updatingText = manga.joinToString("\n") { it.title }
|
||||
progressNotificationBuilder
|
||||
.setContentTitle(context.getString(R.string.notification_updating, current, total))
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(updatingText))
|
||||
}
|
||||
|
||||
context.notificationManager.notify(
|
||||
Notifications.ID_LIBRARY_PROGRESS,
|
||||
progressNotificationBuilder
|
||||
.setContentTitle(title.chop(40))
|
||||
.setContentText("($current/$total)")
|
||||
.setProgress(total, current, false)
|
||||
.build()
|
||||
)
|
||||
|
|
|
@ -30,6 +30,7 @@ import eu.kanade.tachiyomi.source.model.toSManga
|
|||
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
|
||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithTrackServiceTwoWay
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import eu.kanade.tachiyomi.util.prepUpdateCover
|
||||
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
|
@ -47,10 +48,14 @@ import kotlinx.coroutines.awaitAll
|
|||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
/**
|
||||
|
@ -270,50 +275,76 @@ class LibraryUpdateService(
|
|||
* @return an observable delivering the progress of each update.
|
||||
*/
|
||||
suspend fun updateChapterList() {
|
||||
val semaphore = Semaphore(5)
|
||||
val progressCount = AtomicInteger(0)
|
||||
val newUpdates = mutableListOf<Pair<LibraryManga, Array<Chapter>>>()
|
||||
val failedUpdates = mutableListOf<Pair<Manga, String?>>()
|
||||
var hasDownloads = false
|
||||
val currentlyUpdatingManga = CopyOnWriteArrayList<LibraryManga>()
|
||||
val newUpdates = CopyOnWriteArrayList<Pair<LibraryManga, Array<Chapter>>>()
|
||||
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
||||
val hasDownloads = AtomicBoolean(false)
|
||||
val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
|
||||
|
||||
mangaToUpdate.forEach { manga ->
|
||||
if (updateJob?.isActive != true) {
|
||||
return
|
||||
}
|
||||
withIOContext {
|
||||
mangaToUpdate.groupBy { it.source }
|
||||
.values
|
||||
.map { mangaInSource ->
|
||||
async {
|
||||
semaphore.withPermit {
|
||||
mangaInSource.forEach { manga ->
|
||||
if (updateJob?.isActive != true) {
|
||||
return@async
|
||||
}
|
||||
|
||||
notifier.showProgressNotification(manga, progressCount.andIncrement, mangaToUpdate.size)
|
||||
currentlyUpdatingManga.add(manga)
|
||||
progressCount.andIncrement
|
||||
notifier.showProgressNotification(
|
||||
currentlyUpdatingManga,
|
||||
progressCount.get(),
|
||||
mangaToUpdate.size
|
||||
)
|
||||
|
||||
try {
|
||||
val (newChapters, _) = updateManga(manga)
|
||||
try {
|
||||
val (newChapters, _) = updateManga(manga)
|
||||
|
||||
if (newChapters.isNotEmpty()) {
|
||||
if (manga.shouldDownloadNewChapters(db, preferences)) {
|
||||
downloadChapters(manga, newChapters)
|
||||
hasDownloads = true
|
||||
if (newChapters.isNotEmpty()) {
|
||||
if (manga.shouldDownloadNewChapters(db, preferences)) {
|
||||
downloadChapters(manga, newChapters)
|
||||
hasDownloads.set(true)
|
||||
}
|
||||
|
||||
// Convert to the manga that contains new chapters
|
||||
newUpdates.add(manga to newChapters.sortedByDescending { ch -> ch.source_order }.toTypedArray())
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
val errorMessage = if (e is NoChaptersException) {
|
||||
getString(R.string.no_chapters_error)
|
||||
} else {
|
||||
e.message
|
||||
}
|
||||
failedUpdates.add(manga to errorMessage)
|
||||
}
|
||||
|
||||
if (preferences.autoUpdateTrackers()) {
|
||||
updateTrackings(manga, loggedServices)
|
||||
}
|
||||
|
||||
currentlyUpdatingManga.remove(manga)
|
||||
notifier.showProgressNotification(
|
||||
currentlyUpdatingManga,
|
||||
progressCount.get(),
|
||||
mangaToUpdate.size
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to the manga that contains new chapters
|
||||
newUpdates.add(manga to newChapters.sortedByDescending { ch -> ch.source_order }.toTypedArray())
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
val errorMessage = if (e is NoChaptersException) {
|
||||
getString(R.string.no_chapters_error)
|
||||
} else {
|
||||
e.message
|
||||
}
|
||||
failedUpdates.add(manga to errorMessage)
|
||||
}
|
||||
|
||||
if (preferences.autoUpdateTrackers()) {
|
||||
updateTrackings(manga, loggedServices)
|
||||
}
|
||||
.awaitAll()
|
||||
}
|
||||
|
||||
notifier.cancelProgressNotification()
|
||||
|
||||
if (newUpdates.isNotEmpty()) {
|
||||
notifier.showUpdateNotifications(newUpdates)
|
||||
if (hasDownloads) {
|
||||
if (hasDownloads.get()) {
|
||||
DownloadService.start(this)
|
||||
}
|
||||
}
|
||||
|
@ -369,29 +400,56 @@ class LibraryUpdateService(
|
|||
}
|
||||
|
||||
private suspend fun updateCovers() {
|
||||
var progressCount = 0
|
||||
val semaphore = Semaphore(5)
|
||||
val progressCount = AtomicInteger(0)
|
||||
val currentlyUpdatingManga = CopyOnWriteArrayList<LibraryManga>()
|
||||
|
||||
mangaToUpdate.forEach { manga ->
|
||||
if (updateJob?.isActive != true) {
|
||||
return
|
||||
}
|
||||
withIOContext {
|
||||
mangaToUpdate.groupBy { it.source }
|
||||
.values
|
||||
.map { mangaInSource ->
|
||||
async {
|
||||
semaphore.withPermit {
|
||||
mangaInSource.forEach { manga ->
|
||||
if (updateJob?.isActive != true) {
|
||||
return@async
|
||||
}
|
||||
|
||||
notifier.showProgressNotification(manga, progressCount++, mangaToUpdate.size)
|
||||
currentlyUpdatingManga.add(manga)
|
||||
progressCount.andIncrement
|
||||
notifier.showProgressNotification(
|
||||
currentlyUpdatingManga,
|
||||
progressCount.get(),
|
||||
mangaToUpdate.size
|
||||
)
|
||||
|
||||
sourceManager.get(manga.source)?.let { source ->
|
||||
try {
|
||||
val networkManga = source.getMangaDetails(manga.toMangaInfo())
|
||||
val sManga = networkManga.toSManga()
|
||||
manga.prepUpdateCover(coverCache, sManga, true)
|
||||
sManga.thumbnail_url?.let {
|
||||
manga.thumbnail_url = it
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
sourceManager.get(manga.source)?.let { source ->
|
||||
try {
|
||||
val networkManga =
|
||||
source.getMangaDetails(manga.toMangaInfo())
|
||||
val sManga = networkManga.toSManga()
|
||||
manga.prepUpdateCover(coverCache, sManga, true)
|
||||
sManga.thumbnail_url?.let {
|
||||
manga.thumbnail_url = it
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
// Ignore errors and continue
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
|
||||
currentlyUpdatingManga.remove(manga)
|
||||
notifier.showProgressNotification(
|
||||
currentlyUpdatingManga,
|
||||
progressCount.get(),
|
||||
mangaToUpdate.size
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
// Ignore errors and continue
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
}
|
||||
|
||||
coverCache.clearMemoryCache()
|
||||
|
@ -411,8 +469,7 @@ class LibraryUpdateService(
|
|||
return
|
||||
}
|
||||
|
||||
// Notify manga that will update.
|
||||
notifier.showProgressNotification(manga, progressCount++, mangaToUpdate.size)
|
||||
notifier.showProgressNotification(listOf(manga), progressCount++, mangaToUpdate.size)
|
||||
|
||||
// Update the tracking details.
|
||||
updateTrackings(manga, loggedServices)
|
||||
|
|
|
@ -28,6 +28,7 @@ object PreferenceValues {
|
|||
MIDNIGHT_DUSK(R.string.theme_midnightdusk),
|
||||
STRAWBERRY_DAIQUIRI(R.string.theme_strawberrydaiquiri),
|
||||
YOTSUBA(R.string.theme_yotsuba),
|
||||
YINYANG(R.string.theme_yinyang),
|
||||
|
||||
// Deprecated
|
||||
DARK_BLUE(null),
|
||||
|
|
|
@ -22,11 +22,23 @@ internal class AnimeExtensionGithubApi {
|
|||
private val networkService: NetworkHelper by injectLazy()
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
private var requiresFallbackSource = false
|
||||
|
||||
suspend fun findExtensions(): List<AnimeExtension.Available> {
|
||||
return withIOContext {
|
||||
networkService.client
|
||||
.newCall(GET("${REPO_URL_PREFIX}index.min.json"))
|
||||
.await()
|
||||
val response = try {
|
||||
networkService.client
|
||||
.newCall(GET("${REPO_URL_PREFIX}index.min.json"))
|
||||
.await()
|
||||
} catch (e: Throwable) {
|
||||
requiresFallbackSource = true
|
||||
|
||||
networkService.client
|
||||
.newCall(GET("${FALLBACK_REPO_URL_PREFIX}index.min.json"))
|
||||
.await()
|
||||
}
|
||||
|
||||
response
|
||||
.parseAs<JsonArray>()
|
||||
.let { parseResponse(it) }
|
||||
}
|
||||
|
@ -59,7 +71,7 @@ internal class AnimeExtensionGithubApi {
|
|||
return json
|
||||
.filter { element ->
|
||||
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
|
||||
val libVersion = versionName.substringBeforeLast('.').toInt()
|
||||
val libVersion = versionName.substringBeforeLast('.').toDouble()
|
||||
libVersion >= AnimeExtensionLoader.LIB_VERSION_MIN && libVersion <= AnimeExtensionLoader.LIB_VERSION_MAX
|
||||
}
|
||||
.map { element ->
|
||||
|
@ -70,18 +82,23 @@ internal class AnimeExtensionGithubApi {
|
|||
val versionCode = element.jsonObject["code"]!!.jsonPrimitive.int
|
||||
val lang = element.jsonObject["lang"]!!.jsonPrimitive.content
|
||||
val nsfw = element.jsonObject["nsfw"]!!.jsonPrimitive.int == 1
|
||||
val icon = "${REPO_URL_PREFIX}icon/${apkName.replace(".apk", ".png")}"
|
||||
val icon = "${getUrlPrefix()}icon/${apkName.replace(".apk", ".png")}"
|
||||
|
||||
AnimeExtension.Available(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon)
|
||||
}
|
||||
}
|
||||
|
||||
fun getApkUrl(extension: AnimeExtension.Available): String {
|
||||
return "${REPO_URL_PREFIX}apk/${extension.apkName}"
|
||||
return "${getUrlPrefix()}apk/${extension.apkName}"
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val BASE_URL = "https://raw.githubusercontent.com/"
|
||||
const val REPO_URL_PREFIX = "${BASE_URL}jmir1/aniyomi-extensions/repo/"
|
||||
private fun getUrlPrefix(): String {
|
||||
return when (requiresFallbackSource) {
|
||||
true -> FALLBACK_REPO_URL_PREFIX
|
||||
false -> REPO_URL_PREFIX
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val REPO_URL_PREFIX = "https://raw.githubusercontent.com/jmir1/aniyomi-extensions/repo/"
|
||||
private const val FALLBACK_REPO_URL_PREFIX = "https://cdn.jsdelivr.net/gh/jmir1/aniyomi-extensions@repo/"
|
||||
|
|
|
@ -22,11 +22,23 @@ 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 {
|
||||
networkService.client
|
||||
.newCall(GET("${REPO_URL_PREFIX}index.min.json"))
|
||||
.await()
|
||||
val response = try {
|
||||
networkService.client
|
||||
.newCall(GET("${REPO_URL_PREFIX}index.min.json"))
|
||||
.await()
|
||||
} catch (e: Throwable) {
|
||||
requiresFallbackSource = true
|
||||
|
||||
networkService.client
|
||||
.newCall(GET("${FALLBACK_REPO_URL_PREFIX}index.min.json"))
|
||||
.await()
|
||||
}
|
||||
|
||||
response
|
||||
.parseAs<JsonArray>()
|
||||
.let { parseResponse(it) }
|
||||
}
|
||||
|
@ -70,18 +82,23 @@ internal class ExtensionGithubApi {
|
|||
val versionCode = element.jsonObject["code"]!!.jsonPrimitive.int
|
||||
val lang = element.jsonObject["lang"]!!.jsonPrimitive.content
|
||||
val nsfw = element.jsonObject["nsfw"]!!.jsonPrimitive.int == 1
|
||||
val icon = "${REPO_URL_PREFIX}icon/${apkName.replace(".apk", ".png")}"
|
||||
val icon = "${getUrlPrefix()}icon/${apkName.replace(".apk", ".png")}"
|
||||
|
||||
Extension.Available(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon)
|
||||
}
|
||||
}
|
||||
|
||||
fun getApkUrl(extension: Extension.Available): String {
|
||||
return "${REPO_URL_PREFIX}apk/${extension.apkName}"
|
||||
return "${getUrlPrefix()}apk/${extension.apkName}"
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val BASE_URL = "https://raw.githubusercontent.com/"
|
||||
const val REPO_URL_PREFIX = "${BASE_URL}tachiyomiorg/tachiyomi-extensions/repo/"
|
||||
private fun getUrlPrefix(): String {
|
||||
return when (requiresFallbackSource) {
|
||||
true -> FALLBACK_REPO_URL_PREFIX
|
||||
false -> 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://cdn.jsdelivr.net/gh/tachiyomiorg/tachiyomi-extensions@repo/"
|
||||
|
|
|
@ -29,7 +29,6 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||
const val ID = 0L
|
||||
const val HELP_URL = "https://tachiyomi.org/help/guides/local-manga/"
|
||||
|
||||
private const val COVER_NAME = "cover.jpg"
|
||||
private val SUPPORTED_ARCHIVE_TYPES = setOf("zip", "rar", "cbr", "cbz", "epub")
|
||||
|
||||
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
|
||||
|
@ -40,18 +39,29 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||
input.close()
|
||||
return null
|
||||
}
|
||||
val cover = File("${dir.absolutePath}/${manga.url}", COVER_NAME)
|
||||
val cover = getCoverFile(File("${dir.absolutePath}/${manga.url}"))
|
||||
|
||||
// It might not exist if using the external SD card
|
||||
cover.parentFile?.mkdirs()
|
||||
input.use {
|
||||
cover.outputStream().use {
|
||||
input.copyTo(it)
|
||||
if (cover != null && cover.exists()) {
|
||||
// It might not exist if using the external SD card
|
||||
cover.parentFile?.mkdirs()
|
||||
input.use {
|
||||
cover.outputStream().use {
|
||||
input.copyTo(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
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) }
|
||||
|
@ -105,8 +115,8 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||
|
||||
// Try to find the cover
|
||||
for (dir in baseDirs) {
|
||||
val cover = File("${dir.absolutePath}/$url", COVER_NAME)
|
||||
if (cover.exists()) {
|
||||
val cover = getCoverFile(File("${dir.absolutePath}/$url"))
|
||||
if (cover != null && cover.exists()) {
|
||||
thumbnail_url = cover.absolutePath
|
||||
break
|
||||
}
|
||||
|
|
|
@ -655,6 +655,29 @@ class AnimeController :
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a genre search using the provided genre name.
|
||||
*
|
||||
* @param genreName the search genre to the parent controller
|
||||
*/
|
||||
fun performGenreSearch(genreName: String) {
|
||||
if (router.backstackSize < 2) {
|
||||
return
|
||||
}
|
||||
|
||||
val previousController = router.backstack[router.backstackSize - 2].controller
|
||||
val presenterSource = presenter.source
|
||||
|
||||
if (previousController is BrowseAnimeSourceController &&
|
||||
presenterSource is AnimeHttpSource
|
||||
) {
|
||||
router.handleBack()
|
||||
previousController.searchWithGenre(genreName)
|
||||
} else {
|
||||
performSearch(genreName)
|
||||
}
|
||||
}
|
||||
|
||||
private fun shareCover() {
|
||||
try {
|
||||
val activity = activity!!
|
||||
|
|
|
@ -262,11 +262,11 @@ class AnimeInfoHeaderAdapter(
|
|||
if (!anime.genre.isNullOrBlank()) {
|
||||
binding.mangaGenresTagsCompactChips.setChips(
|
||||
anime.getGenres(),
|
||||
controller::performSearch
|
||||
controller::performGenreSearch
|
||||
)
|
||||
binding.mangaGenresTagsFullChips.setChips(
|
||||
anime.getGenres(),
|
||||
controller::performSearch
|
||||
controller::performGenreSearch
|
||||
)
|
||||
} else {
|
||||
binding.mangaGenresTagsCompactChips.isVisible = false
|
||||
|
|
|
@ -56,6 +56,9 @@ abstract class BaseThemedActivity : AppCompatActivity() {
|
|||
PreferenceValues.AppTheme.YOTSUBA -> {
|
||||
resIds += R.style.Theme_Tachiyomi_Yotsuba
|
||||
}
|
||||
PreferenceValues.AppTheme.YINYANG -> {
|
||||
resIds += R.style.Theme_Tachiyomi_YinYang
|
||||
}
|
||||
else -> {
|
||||
resIds += R.style.Theme_Tachiyomi
|
||||
}
|
||||
|
|
|
@ -60,7 +60,7 @@ class BrowseController :
|
|||
binding.pager.adapter = adapter
|
||||
|
||||
if (toExtensions) {
|
||||
binding.pager.currentItem = EXTENSIONS_CONTROLLER
|
||||
binding.pager.currentItem = ANIMEEXTENSIONS_CONTROLLER
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import eu.davidea.flexibleadapter.items.IFlexible
|
|||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
|
||||
import eu.kanade.tachiyomi.animesource.LocalAnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||
import eu.kanade.tachiyomi.data.database.models.Anime
|
||||
|
@ -335,6 +336,54 @@ open class BrowseAnimeSourceController(bundle: Bundle) :
|
|||
presenter.restartPager(newQuery)
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to restart the request with a new genre-filtered query.
|
||||
* If the genre name can't be found the filters,
|
||||
* the standard searchWithQuery search method is used instead.
|
||||
*
|
||||
* @param genreName the name of the genre
|
||||
*/
|
||||
fun searchWithGenre(genreName: String) {
|
||||
presenter.sourceFilters = presenter.source.getFilterList()
|
||||
|
||||
var filterList: AnimeFilterList? = null
|
||||
|
||||
filter@ for (sourceFilter in presenter.sourceFilters) {
|
||||
if (sourceFilter is AnimeFilter.Group<*>) {
|
||||
for (filter in sourceFilter.state) {
|
||||
if (filter is AnimeFilter<*> && filter.name.equals(genreName, true)) {
|
||||
when (filter) {
|
||||
is AnimeFilter.TriState -> filter.state = 1
|
||||
is AnimeFilter.CheckBox -> filter.state = true
|
||||
}
|
||||
filterList = presenter.sourceFilters
|
||||
break@filter
|
||||
}
|
||||
}
|
||||
} else if (sourceFilter is AnimeFilter.Select<*>) {
|
||||
val index = sourceFilter.values.filterIsInstance<String>()
|
||||
.indexOfFirst { it.equals(genreName, true) }
|
||||
|
||||
if (index != -1) {
|
||||
sourceFilter.state = index
|
||||
filterList = presenter.sourceFilters
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (filterList != null) {
|
||||
filterSheet?.setFilters(presenter.filterItems)
|
||||
|
||||
showProgressBar()
|
||||
|
||||
adapter?.clear()
|
||||
presenter.restartPager("", filterList)
|
||||
} else {
|
||||
searchWithQuery(genreName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the presenter when the network request is received.
|
||||
*
|
||||
|
|
|
@ -29,6 +29,7 @@ import eu.kanade.tachiyomi.data.preference.asImmediateFlow
|
|||
import eu.kanade.tachiyomi.databinding.SourceControllerBinding
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.base.controller.FabController
|
||||
|
@ -335,6 +336,54 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||
presenter.restartPager(newQuery)
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to restart the request with a new genre-filtered query.
|
||||
* If the genre name can't be found the filters,
|
||||
* the standard searchWithQuery search method is used instead.
|
||||
*
|
||||
* @param genreName the name of the genre
|
||||
*/
|
||||
fun searchWithGenre(genreName: String) {
|
||||
presenter.sourceFilters = presenter.source.getFilterList()
|
||||
|
||||
var filterList: FilterList? = null
|
||||
|
||||
filter@ for (sourceFilter in presenter.sourceFilters) {
|
||||
if (sourceFilter is Filter.Group<*>) {
|
||||
for (filter in sourceFilter.state) {
|
||||
if (filter is Filter<*> && filter.name.equals(genreName, true)) {
|
||||
when (filter) {
|
||||
is Filter.TriState -> filter.state = 1
|
||||
is Filter.CheckBox -> filter.state = true
|
||||
}
|
||||
filterList = presenter.sourceFilters
|
||||
break@filter
|
||||
}
|
||||
}
|
||||
} else if (sourceFilter is Filter.Select<*>) {
|
||||
val index = sourceFilter.values.filterIsInstance<String>()
|
||||
.indexOfFirst { it.equals(genreName, true) }
|
||||
|
||||
if (index != -1) {
|
||||
sourceFilter.state = index
|
||||
filterList = presenter.sourceFilters
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (filterList != null) {
|
||||
filterSheet?.setFilters(presenter.filterItems)
|
||||
|
||||
showProgressBar()
|
||||
|
||||
adapter?.clear()
|
||||
presenter.restartPager("", filterList)
|
||||
} else {
|
||||
searchWithQuery(genreName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the presenter when the network request is received.
|
||||
*
|
||||
|
|
|
@ -19,7 +19,6 @@ import com.bluelinelabs.conductor.Conductor
|
|||
import com.bluelinelabs.conductor.Controller
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
import com.bluelinelabs.conductor.Router
|
||||
import com.bluelinelabs.conductor.RouterTransaction
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.behavior.HideBottomViewOnScrollBehavior
|
||||
import com.google.android.material.navigation.NavigationBarView
|
||||
|
@ -44,6 +43,7 @@ import eu.kanade.tachiyomi.ui.base.controller.ToolbarLiftOnScrollController
|
|||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.browse.BrowseController
|
||||
import eu.kanade.tachiyomi.ui.browse.animesource.browse.BrowseAnimeSourceController
|
||||
import eu.kanade.tachiyomi.ui.browse.animesource.globalsearch.GlobalAnimeSearchController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
||||
import eu.kanade.tachiyomi.ui.download.DownloadTabsController
|
||||
|
@ -331,7 +331,7 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
|
|||
router.popToRoot()
|
||||
}
|
||||
setSelectedNavItem(R.id.nav_browse)
|
||||
router.pushController(BrowseController(true).withFadeTransaction())
|
||||
router.pushController(BrowseController(toExtensions = true).withFadeTransaction())
|
||||
}
|
||||
SHORTCUT_MANGA -> {
|
||||
val extras = intent.extras ?: return false
|
||||
|
@ -339,7 +339,7 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
|
|||
router.popToRoot()
|
||||
}
|
||||
setSelectedNavItem(R.id.nav_library)
|
||||
router.pushController(RouterTransaction.with(MangaController(extras)))
|
||||
router.pushController(MangaController(extras).withFadeTransaction())
|
||||
}
|
||||
SHORTCUT_ANIME -> {
|
||||
val extras = intent.extras ?: return false
|
||||
|
@ -347,21 +347,21 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
|
|||
router.popToRoot()
|
||||
}
|
||||
setSelectedNavItem(R.id.nav_animelib)
|
||||
router.pushController(RouterTransaction.with(AnimeController(extras)))
|
||||
router.pushController(AnimeController(extras).withFadeTransaction())
|
||||
}
|
||||
SHORTCUT_DOWNLOADS -> {
|
||||
if (router.backstackSize > 1) {
|
||||
router.popToRoot()
|
||||
}
|
||||
setSelectedNavItem(R.id.nav_updates)
|
||||
router.pushController(RouterTransaction.with(MangaDownloadController()))
|
||||
setSelectedNavItem(R.id.nav_more)
|
||||
router.pushController(MangaDownloadController().withFadeTransaction())
|
||||
}
|
||||
SHORTCUT_ANIME_DOWNLOADS -> {
|
||||
if (router.backstackSize > 1) {
|
||||
router.popToRoot()
|
||||
}
|
||||
setSelectedNavItem(R.id.nav_updates)
|
||||
router.pushController(RouterTransaction.with(AnimeDownloadController()))
|
||||
setSelectedNavItem(R.id.nav_more)
|
||||
router.pushController(AnimeDownloadController().withFadeTransaction())
|
||||
}
|
||||
Intent.ACTION_SEARCH, Intent.ACTION_SEND, "com.google.android.gms.actions.SEARCH_ACTION" -> {
|
||||
// If the intent match the "standard" Android search intent
|
||||
|
@ -378,14 +378,24 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
|
|||
}
|
||||
INTENT_SEARCH -> {
|
||||
val query = intent.getStringExtra(INTENT_SEARCH_QUERY)
|
||||
val filter = intent.getStringExtra(INTENT_SEARCH_FILTER)
|
||||
if (query != null && query.isNotEmpty()) {
|
||||
val filter = intent.getStringExtra(INTENT_SEARCH_FILTER)
|
||||
if (router.backstackSize > 1) {
|
||||
router.popToRoot()
|
||||
}
|
||||
router.pushController(GlobalSearchController(query, filter).withFadeTransaction())
|
||||
}
|
||||
}
|
||||
INTENT_ANIMESEARCH -> {
|
||||
val query = intent.getStringExtra(INTENT_SEARCH_QUERY)
|
||||
if (query != null && query.isNotEmpty()) {
|
||||
val filter = intent.getStringExtra(INTENT_SEARCH_FILTER)
|
||||
if (router.backstackSize > 1) {
|
||||
router.popToRoot()
|
||||
}
|
||||
router.pushController(GlobalAnimeSearchController(query, filter).withFadeTransaction())
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
isHandlingShortcut = false
|
||||
return false
|
||||
|
@ -576,6 +586,7 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
|
|||
const val SHORTCUT_EXTENSIONS = "eu.kanade.tachiyomi.EXTENSIONS"
|
||||
|
||||
const val INTENT_SEARCH = "eu.kanade.tachiyomi.SEARCH"
|
||||
const val INTENT_ANIMESEARCH = "eu.kanade.tachiyomi.ANIMESEARCH"
|
||||
const val INTENT_SEARCH_QUERY = "query"
|
||||
const val INTENT_SEARCH_FILTER = "filter"
|
||||
}
|
||||
|
|
|
@ -638,6 +638,29 @@ class MangaController :
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a genre search using the provided genre name.
|
||||
*
|
||||
* @param genreName the search genre to the parent controller
|
||||
*/
|
||||
fun performGenreSearch(genreName: String) {
|
||||
if (router.backstackSize < 2) {
|
||||
return
|
||||
}
|
||||
|
||||
val previousController = router.backstack[router.backstackSize - 2].controller
|
||||
val presenterSource = presenter.source
|
||||
|
||||
if (previousController is BrowseSourceController &&
|
||||
presenterSource is HttpSource
|
||||
) {
|
||||
router.handleBack()
|
||||
previousController.searchWithGenre(genreName)
|
||||
} else {
|
||||
performSearch(genreName)
|
||||
}
|
||||
}
|
||||
|
||||
private fun shareCover() {
|
||||
try {
|
||||
val activity = activity!!
|
||||
|
|
|
@ -262,11 +262,11 @@ class MangaInfoHeaderAdapter(
|
|||
if (!manga.genre.isNullOrBlank()) {
|
||||
binding.mangaGenresTagsCompactChips.setChips(
|
||||
manga.getGenres(),
|
||||
controller::performSearch
|
||||
controller::performGenreSearch
|
||||
)
|
||||
binding.mangaGenresTagsFullChips.setChips(
|
||||
manga.getGenres(),
|
||||
controller::performSearch
|
||||
controller::performGenreSearch
|
||||
)
|
||||
} else {
|
||||
binding.mangaGenresTagsCompactChips.isVisible = false
|
||||
|
|
|
@ -8,6 +8,7 @@ import android.content.Intent
|
|||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
|
@ -35,6 +36,7 @@ import eu.kanade.tachiyomi.util.preference.preference
|
|||
import eu.kanade.tachiyomi.util.preference.preferenceCategory
|
||||
import eu.kanade.tachiyomi.util.preference.summaryRes
|
||||
import eu.kanade.tachiyomi.util.preference.titleRes
|
||||
import eu.kanade.tachiyomi.util.system.MiuiUtil
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
@ -76,6 +78,11 @@ class SettingsBackupController : SettingsController() {
|
|||
summaryRes = R.string.pref_restore_backup_summ
|
||||
|
||||
onClick {
|
||||
if (MiuiUtil.isMiui() && MiuiUtil.isMiuiOptimizationDisabled()) {
|
||||
context.toast(R.string.restore_miui_warning, Toast.LENGTH_LONG)
|
||||
return@onClick
|
||||
}
|
||||
|
||||
if (!BackupRestoreService.isRunning(context)) {
|
||||
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
package eu.kanade.tachiyomi.util.system
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import timber.log.Timber
|
||||
|
||||
object MiuiUtil {
|
||||
|
||||
fun isMiui(): Boolean {
|
||||
return getSystemProperty("ro.miui.ui.version.name")?.isNotEmpty() ?: false
|
||||
}
|
||||
|
||||
@SuppressLint("PrivateApi")
|
||||
fun isMiuiOptimizationDisabled(): Boolean {
|
||||
if ("0" == getSystemProperty("persist.sys.miui_optimization")) {
|
||||
return true
|
||||
}
|
||||
|
||||
return try {
|
||||
Class.forName("android.miui.AppOpsUtils")
|
||||
.getDeclaredMethod("isXOptMode")
|
||||
.invoke(null) as Boolean
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("PrivateApi")
|
||||
private fun getSystemProperty(key: String?): String? {
|
||||
return try {
|
||||
Class.forName("android.os.SystemProperties")
|
||||
.getDeclaredMethod("get", String::class.java)
|
||||
.invoke(null, key) as String
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e, "Unable to use SystemProperties.get")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,9 +2,15 @@
|
|||
<resources>
|
||||
<color name="splash">@color/background_default</color>
|
||||
|
||||
<!-- Default Theme -->
|
||||
<color name="accent_default">#3399FF</color>
|
||||
<color name="divider_default">@color/md_white_1000_12</color>
|
||||
<color name="surface_default">#242529</color>
|
||||
<color name="background_default">#202125</color>
|
||||
<color name="ripple_colored_default">#1F3399FF</color>
|
||||
|
||||
<!-- Yin Yang Theme -->
|
||||
<color name="accent_yinyang">#FFFFFF</color>
|
||||
<color name="color_on_secondary_yinyang">#000000</color>
|
||||
<color name="ripple_colored_yinyang">#777777</color>
|
||||
</resources>
|
||||
|
|
|
@ -19,4 +19,14 @@
|
|||
<item name="android:colorBackground">@color/background_midnightdusk</item>
|
||||
</style>
|
||||
|
||||
<!--== Green Apple theme ==-->
|
||||
<style name="Theme.Tachiyomi.GreenApple">
|
||||
<!-- Theme colors -->
|
||||
<item name="colorPrimary">@color/accent_greenapple</item>
|
||||
<item name="colorOnPrimary">@color/md_black_1000</item>
|
||||
<item name="colorTertiary">@color/md_blue_A400</item>
|
||||
<item name="colorControlHighlight">@color/ripple_colored_greenapple</item>
|
||||
<item name="lightSystemBarsOnPrimary">true</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -23,8 +23,14 @@
|
|||
|
||||
<!-- Green Apple Theme -->
|
||||
<color name="accent_greenapple">#48E484</color>
|
||||
<color name="accent_greenapple_variant">#188140</color>
|
||||
<color name="ripple_colored_greenapple">#1F48E484</color>
|
||||
|
||||
<!-- Yin Yang Theme -->
|
||||
<color name="accent_yinyang">#000000</color>
|
||||
<color name="color_on_secondary_yinyang">#FFFFFF</color>
|
||||
<color name="ripple_colored_yinyang">#999999</color>
|
||||
|
||||
<!-- Midnight Dusk Theme -->
|
||||
<color name="accent_midnightdusk">#F02475</color>
|
||||
<color name="background_midnightdusk">#16151D</color>
|
||||
|
|
|
@ -176,6 +176,7 @@
|
|||
<string name="theme_yotsuba">Yotsuba</string>
|
||||
<string name="theme_blue">Blue</string>
|
||||
<string name="theme_greenapple">Green Apple</string>
|
||||
<string name="theme_yinyang">Yin & Yang</string>
|
||||
<string name="theme_midnightdusk">Midnight Dusk</string>
|
||||
<string name="pref_dark_theme_pure_black">Pure black dark mode</string>
|
||||
<string name="pref_start_screen">Start screen</string>
|
||||
|
@ -401,9 +402,9 @@
|
|||
<string name="pref_download_directory">Download location</string>
|
||||
<string name="pref_download_only_over_wifi">Only download over Wi-Fi</string>
|
||||
<string name="pref_category_delete_chapters">Delete chapters</string>
|
||||
<string name="pref_remove_after_marked_as_read">After manually marked as read</string>
|
||||
<string name="pref_remove_after_read">After reading</string>
|
||||
<string name="pref_remove_bookmarked_chapters">Delete bookmarked chapters</string>
|
||||
<string name="pref_remove_after_marked_as_read">After marked as read</string>
|
||||
<string name="pref_remove_after_read">Automatically after reading</string>
|
||||
<string name="pref_remove_bookmarked_chapters">Allow deleting bookmarked chapters</string>
|
||||
<string name="custom_dir">Custom location</string>
|
||||
<string name="disabled">Disabled</string>
|
||||
<string name="last_read_chapter">Last read chapter</string>
|
||||
|
@ -458,6 +459,7 @@
|
|||
<string name="backup_choice">What do you want to backup?</string>
|
||||
<string name="creating_backup">Creating backup</string>
|
||||
<string name="creating_backup_error">Backup failed</string>
|
||||
<string name="restore_miui_warning">MIUI Optimization must be enabled for restore to work correctly.</string>
|
||||
<string name="restore_in_progress">Restore is already in progress</string>
|
||||
<string name="restoring_backup">Restoring backup</string>
|
||||
<string name="restoring_backup_error">Restoring backup failed</string>
|
||||
|
@ -743,6 +745,7 @@
|
|||
|
||||
<!-- 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_new_chapters">New chapters found</string>
|
||||
<string name="notification_new_episodes">New episodes found</string>
|
||||
<plurals name="notification_new_chapters_summary">
|
||||
|
|
|
@ -104,13 +104,23 @@
|
|||
<!--== Green Apple theme ==-->
|
||||
<style name="Theme.Tachiyomi.GreenApple">
|
||||
<!-- Theme colors -->
|
||||
<item name="colorPrimary">@color/accent_greenapple</item>
|
||||
<item name="colorOnPrimary">@color/md_black_1000</item>
|
||||
<item name="colorPrimary">@color/accent_greenapple_variant</item>
|
||||
<item name="colorOnPrimary">@color/md_white_1000</item>
|
||||
<item name="colorTertiary">@color/md_blue_A400</item>
|
||||
<item name="colorControlHighlight">@color/ripple_colored_greenapple</item>
|
||||
<item name="lightSystemBarsOnPrimary">true</item>
|
||||
</style>
|
||||
|
||||
<!--== Yin Yang theme ==-->
|
||||
<style name="Theme.Tachiyomi.YinYang">
|
||||
<!-- Theme colors -->
|
||||
<item name="colorPrimary">@color/accent_yinyang</item>
|
||||
<item name="colorOnSecondary">@color/color_on_secondary_yinyang</item>
|
||||
<item name="colorTertiary">@color/color_on_secondary_yinyang</item>
|
||||
<item name="colorOnTertiary">@color/accent_yinyang</item>
|
||||
<item name="colorControlHighlight">@color/ripple_colored_yinyang</item>
|
||||
</style>
|
||||
|
||||
<!--== Midnight Dusk theme ==-->
|
||||
<style name="Theme.Tachiyomi.MidnightDusk">
|
||||
<!-- Theme colors -->
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
object BuildPluginsVersion {
|
||||
const val AGP = "4.2.2"
|
||||
const val KOTLIN = "1.5.10"
|
||||
const val KOTLINTER = "3.4.4"
|
||||
const val KOTLIN = "1.5.20"
|
||||
const val KOTLINTER = "3.4.5"
|
||||
const val VERSIONS_PLUGIN = "0.39.0"
|
||||
const val ABOUTLIB_PLUGIN = "8.9.0"
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue