mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 06:17:57 +01:00 
			
		
		
		
	Linting fixes
This commit is contained in:
		| @@ -23,12 +23,12 @@ import uy.kohesive.injekt.injectLazy | ||||
| import uy.kohesive.injekt.registry.default.DefaultRegistrar | ||||
|  | ||||
| @AcraCore( | ||||
|         buildConfigClass = BuildConfig::class, | ||||
|         excludeMatchingSharedPreferencesKeys = [".*username.*", ".*password.*", ".*token.*"] | ||||
|     buildConfigClass = BuildConfig::class, | ||||
|     excludeMatchingSharedPreferencesKeys = [".*username.*", ".*password.*", ".*token.*"] | ||||
| ) | ||||
| @AcraHttpSender( | ||||
|         uri = "https://tachiyomi.kanade.eu/crash_report", | ||||
|         httpMethod = HttpSender.Method.PUT | ||||
|     uri = "https://tachiyomi.kanade.eu/crash_report", | ||||
|     httpMethod = HttpSender.Method.PUT | ||||
| ) | ||||
| open class App : Application(), LifecycleObserver { | ||||
|  | ||||
|   | ||||
| @@ -22,7 +22,6 @@ import uy.kohesive.injekt.api.get | ||||
| class AppModule(val app: Application) : InjektModule { | ||||
|  | ||||
|     override fun InjektRegistrar.registerInjectables() { | ||||
|  | ||||
|         addSingleton(app) | ||||
|  | ||||
|         addSingletonFactory { PreferencesHelper(app) } | ||||
|   | ||||
| @@ -13,7 +13,7 @@ import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| class BackupCreatorJob(private val context: Context, workerParams: WorkerParameters) : | ||||
|         Worker(context, workerParams) { | ||||
|     Worker(context, workerParams) { | ||||
|  | ||||
|     override fun doWork(): Result { | ||||
|         val preferences = Injekt.get<PreferencesHelper>() | ||||
| @@ -32,10 +32,11 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet | ||||
|             val interval = prefInterval ?: preferences.backupInterval().get() | ||||
|             if (interval > 0) { | ||||
|                 val request = PeriodicWorkRequestBuilder<BackupCreatorJob>( | ||||
|                         interval.toLong(), TimeUnit.HOURS, | ||||
|                         10, TimeUnit.MINUTES) | ||||
|                         .addTag(TAG) | ||||
|                         .build() | ||||
|                     interval.toLong(), TimeUnit.HOURS, | ||||
|                     10, TimeUnit.MINUTES | ||||
|                 ) | ||||
|                     .addTag(TAG) | ||||
|                     .build() | ||||
|  | ||||
|                 WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request) | ||||
|             } else { | ||||
|   | ||||
| @@ -85,7 +85,8 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|  | ||||
|     private fun initParser(): Gson = when (version) { | ||||
|         1 -> GsonBuilder().create() | ||||
|         2 -> GsonBuilder() | ||||
|         2 -> | ||||
|             GsonBuilder() | ||||
|                 .registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build()) | ||||
|                 .registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build()) | ||||
|                 .registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build()) | ||||
| @@ -142,21 +143,21 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|                 val numberOfBackups = numberOfBackups() | ||||
|                 val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.json""") | ||||
|                 dir.listFiles { _, filename -> backupRegex.matches(filename) } | ||||
|                         .orEmpty() | ||||
|                         .sortedByDescending { it.name } | ||||
|                         .drop(numberOfBackups - 1) | ||||
|                         .forEach { it.delete() } | ||||
|                     .orEmpty() | ||||
|                     .sortedByDescending { it.name } | ||||
|                     .drop(numberOfBackups - 1) | ||||
|                     .forEach { it.delete() } | ||||
|  | ||||
|                 // Create new file to place backup | ||||
|                 val newFile = dir.createFile(Backup.getDefaultFilename()) | ||||
|                         ?: throw Exception("Couldn't create backup file") | ||||
|                     ?: throw Exception("Couldn't create backup file") | ||||
|  | ||||
|                 newFile.openOutputStream().bufferedWriter().use { | ||||
|                     parser.toJson(root, it) | ||||
|                 } | ||||
|             } else { | ||||
|                 val file = UniFile.fromUri(context, uri) | ||||
|                         ?: throw Exception("Couldn't create backup file") | ||||
|                     ?: throw Exception("Couldn't create backup file") | ||||
|                 file.openOutputStream().bufferedWriter().use { | ||||
|                     parser.toJson(root, it) | ||||
|                 } | ||||
| @@ -268,13 +269,13 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|      */ | ||||
|     fun restoreMangaFetchObservable(source: Source, manga: Manga): Observable<Manga> { | ||||
|         return source.fetchMangaDetails(manga) | ||||
|                 .map { networkManga -> | ||||
|                     manga.copyFrom(networkManga) | ||||
|                     manga.favorite = true | ||||
|                     manga.initialized = true | ||||
|                     manga.id = insertManga(manga) | ||||
|                     manga | ||||
|                 } | ||||
|             .map { networkManga -> | ||||
|                 manga.copyFrom(networkManga) | ||||
|                 manga.favorite = true | ||||
|                 manga.initialized = true | ||||
|                 manga.id = insertManga(manga) | ||||
|                 manga | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -286,13 +287,13 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|      */ | ||||
|     fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> { | ||||
|         return source.fetchChapterList(manga) | ||||
|                 .map { syncChaptersWithSource(databaseHelper, it, manga, source) } | ||||
|                 .doOnNext { pair -> | ||||
|                     if (pair.first.isNotEmpty()) { | ||||
|                         chapters.forEach { it.manga_id = manga.id } | ||||
|                         insertChapters(chapters) | ||||
|                     } | ||||
|             .map { syncChaptersWithSource(databaseHelper, it, manga, source) } | ||||
|             .doOnNext { pair -> | ||||
|                 if (pair.first.isNotEmpty()) { | ||||
|                     chapters.forEach { it.manga_id = manga.id } | ||||
|                     insertChapters(chapters) | ||||
|                 } | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -442,8 +443,9 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|         val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking() | ||||
|  | ||||
|         // Return if fetch is needed | ||||
|         if (dbChapters.isEmpty() || dbChapters.size < chapters.size) | ||||
|         if (dbChapters.isEmpty() || dbChapters.size < chapters.size) { | ||||
|             return false | ||||
|         } | ||||
|  | ||||
|         for (chapter in chapters) { | ||||
|             val pos = dbChapters.indexOf(chapter) | ||||
| @@ -468,7 +470,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|      * @return [Manga], null if not found | ||||
|      */ | ||||
|     internal fun getMangaFromDatabase(manga: Manga): Manga? = | ||||
|             databaseHelper.getManga(manga.url, manga.source).executeAsBlocking() | ||||
|         databaseHelper.getManga(manga.url, manga.source).executeAsBlocking() | ||||
|  | ||||
|     /** | ||||
|      * Returns list containing manga from library | ||||
| @@ -476,7 +478,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|      * @return [Manga] from library | ||||
|      */ | ||||
|     internal fun getFavoriteManga(): List<Manga> = | ||||
|             databaseHelper.getFavoriteMangas().executeAsBlocking() | ||||
|         databaseHelper.getFavoriteMangas().executeAsBlocking() | ||||
|  | ||||
|     /** | ||||
|      * Inserts manga and returns id | ||||
| @@ -484,7 +486,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|      * @return id of [Manga], null if not found | ||||
|      */ | ||||
|     internal fun insertManga(manga: Manga): Long? = | ||||
|             databaseHelper.insertManga(manga).executeAsBlocking().insertedId() | ||||
|         databaseHelper.insertManga(manga).executeAsBlocking().insertedId() | ||||
|  | ||||
|     /** | ||||
|      * Inserts list of chapters | ||||
|   | ||||
| @@ -60,7 +60,7 @@ class BackupRestoreService : Service() { | ||||
|          * @return true if the service is running, false otherwise. | ||||
|          */ | ||||
|         private fun isRunning(context: Context): Boolean = | ||||
|                 context.isServiceRunning(BackupRestoreService::class.java) | ||||
|             context.isServiceRunning(BackupRestoreService::class.java) | ||||
|  | ||||
|         /** | ||||
|          * Starts a service to restore a backup from Json | ||||
| @@ -143,7 +143,8 @@ class BackupRestoreService : Service() { | ||||
|         startForeground(Notifications.ID_RESTORE, notifier.showRestoreProgress().build()) | ||||
|  | ||||
|         wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock( | ||||
|                 PowerManager.PARTIAL_WAKE_LOCK, "BackupRestoreService:WakeLock") | ||||
|             PowerManager.PARTIAL_WAKE_LOCK, "BackupRestoreService:WakeLock" | ||||
|         ) | ||||
|         wakeLock.acquire() | ||||
|     } | ||||
|  | ||||
| @@ -182,12 +183,13 @@ class BackupRestoreService : Service() { | ||||
|         subscription?.unsubscribe() | ||||
|  | ||||
|         subscription = Observable.using( | ||||
|                 { db.lowLevel().beginTransaction() }, | ||||
|                 { getRestoreObservable(uri).doOnNext { db.lowLevel().setTransactionSuccessful() } }, | ||||
|                 { executor.execute { db.lowLevel().endTransaction() } }) | ||||
|                 .doAfterTerminate { stopSelf(startId) } | ||||
|                 .subscribeOn(Schedulers.from(executor)) | ||||
|                 .subscribe() | ||||
|             { db.lowLevel().beginTransaction() }, | ||||
|             { getRestoreObservable(uri).doOnNext { db.lowLevel().setTransactionSuccessful() } }, | ||||
|             { executor.execute { db.lowLevel().endTransaction() } } | ||||
|         ) | ||||
|             .doAfterTerminate { stopSelf(startId) } | ||||
|             .subscribeOn(Schedulers.from(executor)) | ||||
|             .subscribe() | ||||
|  | ||||
|         return START_NOT_STICKY | ||||
|     } | ||||
| @@ -202,79 +204,87 @@ class BackupRestoreService : Service() { | ||||
|         val startTime = System.currentTimeMillis() | ||||
|  | ||||
|         return Observable.just(Unit) | ||||
|                 .map { | ||||
|                     val reader = JsonReader(contentResolver.openInputStream(uri)!!.bufferedReader()) | ||||
|                     val json = JsonParser.parseReader(reader).asJsonObject | ||||
|             .map { | ||||
|                 val reader = JsonReader(contentResolver.openInputStream(uri)!!.bufferedReader()) | ||||
|                 val json = JsonParser.parseReader(reader).asJsonObject | ||||
|  | ||||
|                     // Get parser version | ||||
|                     val version = json.get(VERSION)?.asInt ?: 1 | ||||
|                 // Get parser version | ||||
|                 val version = json.get(VERSION)?.asInt ?: 1 | ||||
|  | ||||
|                     // Initialize manager | ||||
|                     backupManager = BackupManager(this, version) | ||||
|                 // Initialize manager | ||||
|                 backupManager = BackupManager(this, version) | ||||
|  | ||||
|                     val mangasJson = json.get(MANGAS).asJsonArray | ||||
|                 val mangasJson = json.get(MANGAS).asJsonArray | ||||
|  | ||||
|                     restoreAmount = mangasJson.size() + 1 // +1 for categories | ||||
|                     restoreProgress = 0 | ||||
|                     errors.clear() | ||||
|                 restoreAmount = mangasJson.size() + 1 // +1 for categories | ||||
|                 restoreProgress = 0 | ||||
|                 errors.clear() | ||||
|  | ||||
|                     // Restore categories | ||||
|                     json.get(CATEGORIES)?.let { | ||||
|                         backupManager.restoreCategories(it.asJsonArray) | ||||
|                         restoreProgress += 1 | ||||
|                         showRestoreProgress(restoreProgress, restoreAmount, "Categories added") | ||||
|                     } | ||||
|  | ||||
|                     mangasJson | ||||
|                 // Restore categories | ||||
|                 json.get(CATEGORIES)?.let { | ||||
|                     backupManager.restoreCategories(it.asJsonArray) | ||||
|                     restoreProgress += 1 | ||||
|                     showRestoreProgress(restoreProgress, restoreAmount, "Categories added") | ||||
|                 } | ||||
|                 .flatMap { Observable.from(it) } | ||||
|                 .concatMap { | ||||
|                     val obj = it.asJsonObject | ||||
|                     val manga = backupManager.parser.fromJson<MangaImpl>(obj.get(MANGA)) | ||||
|                     val chapters = backupManager.parser.fromJson<List<ChapterImpl>>(obj.get(CHAPTERS) | ||||
|                             ?: JsonArray()) | ||||
|                     val categories = backupManager.parser.fromJson<List<String>>(obj.get(CATEGORIES) | ||||
|                             ?: JsonArray()) | ||||
|                     val history = backupManager.parser.fromJson<List<DHistory>>(obj.get(HISTORY) | ||||
|                             ?: JsonArray()) | ||||
|                     val tracks = backupManager.parser.fromJson<List<TrackImpl>>(obj.get(TRACK) | ||||
|                             ?: JsonArray()) | ||||
|  | ||||
|                     val observable = getMangaRestoreObservable(manga, chapters, categories, history, tracks) | ||||
|                     if (observable != null) { | ||||
|                         observable | ||||
|                     } else { | ||||
|                         errors.add(Date() to "${manga.title} - ${getString(R.string.source_not_found)}") | ||||
|                         restoreProgress += 1 | ||||
|                         val content = getString(R.string.dialog_restoring_source_not_found, manga.title.chop(15)) | ||||
|                         showRestoreProgress(restoreProgress, restoreAmount, manga.title, content) | ||||
|                         Observable.just(manga) | ||||
|                     } | ||||
|                 mangasJson | ||||
|             } | ||||
|             .flatMap { Observable.from(it) } | ||||
|             .concatMap { | ||||
|                 val obj = it.asJsonObject | ||||
|                 val manga = backupManager.parser.fromJson<MangaImpl>(obj.get(MANGA)) | ||||
|                 val chapters = backupManager.parser.fromJson<List<ChapterImpl>>( | ||||
|                     obj.get(CHAPTERS) | ||||
|                         ?: JsonArray() | ||||
|                 ) | ||||
|                 val categories = backupManager.parser.fromJson<List<String>>( | ||||
|                     obj.get(CATEGORIES) | ||||
|                         ?: JsonArray() | ||||
|                 ) | ||||
|                 val history = backupManager.parser.fromJson<List<DHistory>>( | ||||
|                     obj.get(HISTORY) | ||||
|                         ?: JsonArray() | ||||
|                 ) | ||||
|                 val tracks = backupManager.parser.fromJson<List<TrackImpl>>( | ||||
|                     obj.get(TRACK) | ||||
|                         ?: JsonArray() | ||||
|                 ) | ||||
|  | ||||
|                 val observable = getMangaRestoreObservable(manga, chapters, categories, history, tracks) | ||||
|                 if (observable != null) { | ||||
|                     observable | ||||
|                 } else { | ||||
|                     errors.add(Date() to "${manga.title} - ${getString(R.string.source_not_found)}") | ||||
|                     restoreProgress += 1 | ||||
|                     val content = getString(R.string.dialog_restoring_source_not_found, manga.title.chop(15)) | ||||
|                     showRestoreProgress(restoreProgress, restoreAmount, manga.title, content) | ||||
|                     Observable.just(manga) | ||||
|                 } | ||||
|                 .toList() | ||||
|                 .doOnNext { | ||||
|                     val endTime = System.currentTimeMillis() | ||||
|                     val time = endTime - startTime | ||||
|                     val logFile = writeErrorLog() | ||||
|                     val completeIntent = Intent(BackupConst.INTENT_FILTER).apply { | ||||
|                         putExtra(BackupConst.EXTRA_TIME, time) | ||||
|                         putExtra(BackupConst.EXTRA_ERRORS, errors.size) | ||||
|                         putExtra(BackupConst.EXTRA_ERROR_FILE_PATH, logFile.parent) | ||||
|                         putExtra(BackupConst.EXTRA_ERROR_FILE, logFile.name) | ||||
|                         putExtra(BackupConst.ACTION, BackupConst.ACTION_RESTORE_COMPLETED) | ||||
|                     } | ||||
|                     sendLocalBroadcast(completeIntent) | ||||
|             } | ||||
|             .toList() | ||||
|             .doOnNext { | ||||
|                 val endTime = System.currentTimeMillis() | ||||
|                 val time = endTime - startTime | ||||
|                 val logFile = writeErrorLog() | ||||
|                 val completeIntent = Intent(BackupConst.INTENT_FILTER).apply { | ||||
|                     putExtra(BackupConst.EXTRA_TIME, time) | ||||
|                     putExtra(BackupConst.EXTRA_ERRORS, errors.size) | ||||
|                     putExtra(BackupConst.EXTRA_ERROR_FILE_PATH, logFile.parent) | ||||
|                     putExtra(BackupConst.EXTRA_ERROR_FILE, logFile.name) | ||||
|                     putExtra(BackupConst.ACTION, BackupConst.ACTION_RESTORE_COMPLETED) | ||||
|                 } | ||||
|                 .doOnError { error -> | ||||
|                     Timber.e(error) | ||||
|                     writeErrorLog() | ||||
|                     val errorIntent = Intent(BackupConst.INTENT_FILTER).apply { | ||||
|                         putExtra(BackupConst.ACTION, BackupConst.ACTION_RESTORE_ERROR) | ||||
|                         putExtra(BackupConst.EXTRA_ERROR_MESSAGE, error.message) | ||||
|                     } | ||||
|                     sendLocalBroadcast(errorIntent) | ||||
|                 sendLocalBroadcast(completeIntent) | ||||
|             } | ||||
|             .doOnError { error -> | ||||
|                 Timber.e(error) | ||||
|                 writeErrorLog() | ||||
|                 val errorIntent = Intent(BackupConst.INTENT_FILTER).apply { | ||||
|                     putExtra(BackupConst.ACTION, BackupConst.ACTION_RESTORE_ERROR) | ||||
|                     putExtra(BackupConst.EXTRA_ERROR_MESSAGE, error.message) | ||||
|                 } | ||||
|                 .onErrorReturn { emptyList() } | ||||
|                 sendLocalBroadcast(errorIntent) | ||||
|             } | ||||
|             .onErrorReturn { emptyList() } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -347,28 +357,28 @@ class BackupRestoreService : Service() { | ||||
|         tracks: List<Track> | ||||
|     ): Observable<Manga> { | ||||
|         return backupManager.restoreMangaFetchObservable(source, manga) | ||||
|                 .onErrorReturn { | ||||
|                     errors.add(Date() to "${manga.title} - ${it.message}") | ||||
|                     manga | ||||
|                 } | ||||
|                 .filter { it.id != null } | ||||
|                 .flatMap { | ||||
|                     chapterFetchObservable(source, it, chapters) | ||||
|                             // Convert to the manga that contains new chapters. | ||||
|                             .map { manga } | ||||
|                 } | ||||
|                 .doOnNext { | ||||
|                     restoreExtraForManga(it, categories, history, tracks) | ||||
|                 } | ||||
|                 .flatMap { | ||||
|                     trackingFetchObservable(it, tracks) | ||||
|                             // Convert to the manga that contains new chapters. | ||||
|                             .map { manga } | ||||
|                 } | ||||
|                 .doOnCompleted { | ||||
|                     restoreProgress += 1 | ||||
|                     showRestoreProgress(restoreProgress, restoreAmount, manga.title) | ||||
|                 } | ||||
|             .onErrorReturn { | ||||
|                 errors.add(Date() to "${manga.title} - ${it.message}") | ||||
|                 manga | ||||
|             } | ||||
|             .filter { it.id != null } | ||||
|             .flatMap { | ||||
|                 chapterFetchObservable(source, it, chapters) | ||||
|                     // Convert to the manga that contains new chapters. | ||||
|                     .map { manga } | ||||
|             } | ||||
|             .doOnNext { | ||||
|                 restoreExtraForManga(it, categories, history, tracks) | ||||
|             } | ||||
|             .flatMap { | ||||
|                 trackingFetchObservable(it, tracks) | ||||
|                     // Convert to the manga that contains new chapters. | ||||
|                     .map { manga } | ||||
|             } | ||||
|             .doOnCompleted { | ||||
|                 restoreProgress += 1 | ||||
|                 showRestoreProgress(restoreProgress, restoreAmount, manga.title) | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     private fun mangaNoFetchObservable( | ||||
| @@ -379,28 +389,27 @@ class BackupRestoreService : Service() { | ||||
|         history: List<DHistory>, | ||||
|         tracks: List<Track> | ||||
|     ): Observable<Manga> { | ||||
|  | ||||
|         return Observable.just(backupManga) | ||||
|                 .flatMap { manga -> | ||||
|                     if (!backupManager.restoreChaptersForManga(manga, chapters)) { | ||||
|                         chapterFetchObservable(source, manga, chapters) | ||||
|                                 .map { manga } | ||||
|                     } else { | ||||
|                         Observable.just(manga) | ||||
|                     } | ||||
|                 } | ||||
|                 .doOnNext { | ||||
|                     restoreExtraForManga(it, categories, history, tracks) | ||||
|                 } | ||||
|                 .flatMap { manga -> | ||||
|                     trackingFetchObservable(manga, tracks) | ||||
|                             // Convert to the manga that contains new chapters. | ||||
|                             .map { manga } | ||||
|                 } | ||||
|                 .doOnCompleted { | ||||
|                     restoreProgress += 1 | ||||
|                     showRestoreProgress(restoreProgress, restoreAmount, backupManga.title) | ||||
|             .flatMap { manga -> | ||||
|                 if (!backupManager.restoreChaptersForManga(manga, chapters)) { | ||||
|                     chapterFetchObservable(source, manga, chapters) | ||||
|                         .map { manga } | ||||
|                 } else { | ||||
|                     Observable.just(manga) | ||||
|                 } | ||||
|             } | ||||
|             .doOnNext { | ||||
|                 restoreExtraForManga(it, categories, history, tracks) | ||||
|             } | ||||
|             .flatMap { manga -> | ||||
|                 trackingFetchObservable(manga, tracks) | ||||
|                     // Convert to the manga that contains new chapters. | ||||
|                     .map { manga } | ||||
|             } | ||||
|             .doOnCompleted { | ||||
|                 restoreProgress += 1 | ||||
|                 showRestoreProgress(restoreProgress, restoreAmount, backupManga.title) | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     private fun restoreExtraForManga(manga: Manga, categories: List<String>, history: List<DHistory>, tracks: List<Track>) { | ||||
| @@ -423,11 +432,11 @@ class BackupRestoreService : Service() { | ||||
|      */ | ||||
|     private fun chapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> { | ||||
|         return backupManager.restoreChapterFetchObservable(source, manga, chapters) | ||||
|                 // If there's any error, return empty update and continue. | ||||
|                 .onErrorReturn { | ||||
|                     errors.add(Date() to "${manga.title} - ${it.message}") | ||||
|                     Pair(emptyList(), emptyList()) | ||||
|                 } | ||||
|             // If there's any error, return empty update and continue. | ||||
|             .onErrorReturn { | ||||
|                 errors.add(Date() to "${manga.title} - ${it.message}") | ||||
|                 Pair(emptyList(), emptyList()) | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -438,20 +447,20 @@ class BackupRestoreService : Service() { | ||||
|      */ | ||||
|     private fun trackingFetchObservable(manga: Manga, tracks: List<Track>): Observable<Track> { | ||||
|         return Observable.from(tracks) | ||||
|                 .concatMap { track -> | ||||
|                     val service = trackManager.getService(track.sync_id) | ||||
|                     if (service != null && service.isLogged) { | ||||
|                         service.refresh(track) | ||||
|                                 .doOnNext { db.insertTrack(it).executeAsBlocking() } | ||||
|                                 .onErrorReturn { | ||||
|                                     errors.add(Date() to "${manga.title} - ${it.message}") | ||||
|                                     track | ||||
|                                 } | ||||
|                     } else { | ||||
|                         errors.add(Date() to "${manga.title} - ${service?.name} not logged in") | ||||
|                         Observable.empty() | ||||
|                     } | ||||
|             .concatMap { track -> | ||||
|                 val service = trackManager.getService(track.sync_id) | ||||
|                 if (service != null && service.isLogged) { | ||||
|                     service.refresh(track) | ||||
|                         .doOnNext { db.insertTrack(it).executeAsBlocking() } | ||||
|                         .onErrorReturn { | ||||
|                             errors.add(Date() to "${manga.title} - ${it.message}") | ||||
|                             track | ||||
|                         } | ||||
|                 } else { | ||||
|                     errors.add(Date() to "${manga.title} - ${service?.name} not logged in") | ||||
|                     Observable.empty() | ||||
|                 } | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -46,10 +46,12 @@ class ChapterCache(private val context: Context) { | ||||
|     private val gson: Gson by injectLazy() | ||||
|  | ||||
|     /** Cache class used for cache management.  */ | ||||
|     private val diskCache = DiskLruCache.open(File(context.cacheDir, PARAMETER_CACHE_DIRECTORY), | ||||
|             PARAMETER_APP_VERSION, | ||||
|             PARAMETER_VALUE_COUNT, | ||||
|             PARAMETER_CACHE_SIZE) | ||||
|     private val diskCache = DiskLruCache.open( | ||||
|         File(context.cacheDir, PARAMETER_CACHE_DIRECTORY), | ||||
|         PARAMETER_APP_VERSION, | ||||
|         PARAMETER_VALUE_COUNT, | ||||
|         PARAMETER_CACHE_SIZE | ||||
|     ) | ||||
|  | ||||
|     /** | ||||
|      * Returns directory of cache. | ||||
| @@ -77,8 +79,9 @@ class ChapterCache(private val context: Context) { | ||||
|      */ | ||||
|     fun removeFileFromCache(file: String): Boolean { | ||||
|         // Make sure we don't delete the journal file (keeps track of cache). | ||||
|         if (file == "journal" || file.startsWith("journal.")) | ||||
|         if (file == "journal" || file.startsWith("journal.")) { | ||||
|             return false | ||||
|         } | ||||
|  | ||||
|         return try { | ||||
|             // Remove the extension from the file to get the key of the cache | ||||
|   | ||||
| @@ -21,7 +21,7 @@ class CoverCache(private val context: Context) { | ||||
|      * Cache directory used for cache management. | ||||
|      */ | ||||
|     private val cacheDir = context.getExternalFilesDir("covers") | ||||
|             ?: File(context.filesDir, "covers").also { it.mkdirs() } | ||||
|         ?: File(context.filesDir, "covers").also { it.mkdirs() } | ||||
|  | ||||
|     /** | ||||
|      * Returns the cover from cache. | ||||
| @@ -56,8 +56,9 @@ class CoverCache(private val context: Context) { | ||||
|      */ | ||||
|     fun deleteFromCache(thumbnailUrl: String?): Boolean { | ||||
|         // Check if url is empty. | ||||
|         if (thumbnailUrl.isNullOrEmpty()) | ||||
|         if (thumbnailUrl.isNullOrEmpty()) { | ||||
|             return false | ||||
|         } | ||||
|  | ||||
|         // Remove file. | ||||
|         val file = getCoverFile(thumbnailUrl) | ||||
|   | ||||
| @@ -30,19 +30,19 @@ open class DatabaseHelper(context: Context) : | ||||
|     MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries { | ||||
|  | ||||
|     private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context) | ||||
|             .name(DbOpenCallback.DATABASE_NAME) | ||||
|             .callback(DbOpenCallback()) | ||||
|             .build() | ||||
|         .name(DbOpenCallback.DATABASE_NAME) | ||||
|         .callback(DbOpenCallback()) | ||||
|         .build() | ||||
|  | ||||
|     override val db = DefaultStorIOSQLite.builder() | ||||
|             .sqliteOpenHelper(RequerySQLiteOpenHelperFactory().create(configuration)) | ||||
|             .addTypeMapping(Manga::class.java, MangaTypeMapping()) | ||||
|             .addTypeMapping(Chapter::class.java, ChapterTypeMapping()) | ||||
|             .addTypeMapping(Track::class.java, TrackTypeMapping()) | ||||
|             .addTypeMapping(Category::class.java, CategoryTypeMapping()) | ||||
|             .addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping()) | ||||
|             .addTypeMapping(History::class.java, HistoryTypeMapping()) | ||||
|             .build() | ||||
|         .sqliteOpenHelper(RequerySQLiteOpenHelperFactory().create(configuration)) | ||||
|         .addTypeMapping(Manga::class.java, MangaTypeMapping()) | ||||
|         .addTypeMapping(Chapter::class.java, ChapterTypeMapping()) | ||||
|         .addTypeMapping(Track::class.java, TrackTypeMapping()) | ||||
|         .addTypeMapping(Category::class.java, CategoryTypeMapping()) | ||||
|         .addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping()) | ||||
|         .addTypeMapping(History::class.java, HistoryTypeMapping()) | ||||
|         .build() | ||||
|  | ||||
|     inline fun inTransaction(block: () -> Unit) = db.inTransaction(block) | ||||
|  | ||||
|   | ||||
| @@ -44,8 +44,10 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) { | ||||
|             db.execSQL(ChapterTable.sourceOrderUpdateQuery) | ||||
|  | ||||
|             // Fix kissmanga covers after supporting cloudflare | ||||
|             db.execSQL("""UPDATE mangas SET thumbnail_url = | ||||
|                     REPLACE(thumbnail_url, '93.174.95.110', 'kissmanga.com') WHERE source = 4""") | ||||
|             db.execSQL( | ||||
|                 """UPDATE mangas SET thumbnail_url = | ||||
|                     REPLACE(thumbnail_url, '93.174.95.110', 'kissmanga.com') WHERE source = 4""" | ||||
|             ) | ||||
|         } | ||||
|         if (oldVersion < 3) { | ||||
|             // Initialize history tables | ||||
|   | ||||
| @@ -18,22 +18,22 @@ import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_ORDER | ||||
| import eu.kanade.tachiyomi.data.database.tables.CategoryTable.TABLE | ||||
|  | ||||
| class CategoryTypeMapping : SQLiteTypeMapping<Category>( | ||||
|         CategoryPutResolver(), | ||||
|         CategoryGetResolver(), | ||||
|         CategoryDeleteResolver() | ||||
|     CategoryPutResolver(), | ||||
|     CategoryGetResolver(), | ||||
|     CategoryDeleteResolver() | ||||
| ) | ||||
|  | ||||
| class CategoryPutResolver : DefaultPutResolver<Category>() { | ||||
|  | ||||
|     override fun mapToInsertQuery(obj: Category) = InsertQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .build() | ||||
|  | ||||
|     override fun mapToUpdateQuery(obj: Category) = UpdateQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .where("$COL_ID = ?") | ||||
|             .whereArgs(obj.id) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .where("$COL_ID = ?") | ||||
|         .whereArgs(obj.id) | ||||
|         .build() | ||||
|  | ||||
|     override fun mapToContentValues(obj: Category) = ContentValues(4).apply { | ||||
|         put(COL_ID, obj.id) | ||||
| @@ -56,8 +56,8 @@ class CategoryGetResolver : DefaultGetResolver<Category>() { | ||||
| class CategoryDeleteResolver : DefaultDeleteResolver<Category>() { | ||||
|  | ||||
|     override fun mapToDeleteQuery(obj: Category) = DeleteQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .where("$COL_ID = ?") | ||||
|             .whereArgs(obj.id) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .where("$COL_ID = ?") | ||||
|         .whereArgs(obj.id) | ||||
|         .build() | ||||
| } | ||||
|   | ||||
| @@ -26,22 +26,22 @@ import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_URL | ||||
| import eu.kanade.tachiyomi.data.database.tables.ChapterTable.TABLE | ||||
|  | ||||
| class ChapterTypeMapping : SQLiteTypeMapping<Chapter>( | ||||
|         ChapterPutResolver(), | ||||
|         ChapterGetResolver(), | ||||
|         ChapterDeleteResolver() | ||||
|     ChapterPutResolver(), | ||||
|     ChapterGetResolver(), | ||||
|     ChapterDeleteResolver() | ||||
| ) | ||||
|  | ||||
| class ChapterPutResolver : DefaultPutResolver<Chapter>() { | ||||
|  | ||||
|     override fun mapToInsertQuery(obj: Chapter) = InsertQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .build() | ||||
|  | ||||
|     override fun mapToUpdateQuery(obj: Chapter) = UpdateQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .where("$COL_ID = ?") | ||||
|             .whereArgs(obj.id) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .where("$COL_ID = ?") | ||||
|         .whereArgs(obj.id) | ||||
|         .build() | ||||
|  | ||||
|     override fun mapToContentValues(obj: Chapter) = ContentValues(11).apply { | ||||
|         put(COL_ID, obj.id) | ||||
| @@ -80,8 +80,8 @@ class ChapterGetResolver : DefaultGetResolver<Chapter>() { | ||||
| class ChapterDeleteResolver : DefaultDeleteResolver<Chapter>() { | ||||
|  | ||||
|     override fun mapToDeleteQuery(obj: Chapter) = DeleteQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .where("$COL_ID = ?") | ||||
|             .whereArgs(obj.id) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .where("$COL_ID = ?") | ||||
|         .whereArgs(obj.id) | ||||
|         .build() | ||||
| } | ||||
|   | ||||
| @@ -18,22 +18,22 @@ import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_TIME_READ | ||||
| import eu.kanade.tachiyomi.data.database.tables.HistoryTable.TABLE | ||||
|  | ||||
| class HistoryTypeMapping : SQLiteTypeMapping<History>( | ||||
|         HistoryPutResolver(), | ||||
|         HistoryGetResolver(), | ||||
|         HistoryDeleteResolver() | ||||
|     HistoryPutResolver(), | ||||
|     HistoryGetResolver(), | ||||
|     HistoryDeleteResolver() | ||||
| ) | ||||
|  | ||||
| open class HistoryPutResolver : DefaultPutResolver<History>() { | ||||
|  | ||||
|     override fun mapToInsertQuery(obj: History) = InsertQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .build() | ||||
|  | ||||
|     override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .where("$COL_ID = ?") | ||||
|             .whereArgs(obj.id) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .where("$COL_ID = ?") | ||||
|         .whereArgs(obj.id) | ||||
|         .build() | ||||
|  | ||||
|     override fun mapToContentValues(obj: History) = ContentValues(4).apply { | ||||
|         put(COL_ID, obj.id) | ||||
| @@ -56,8 +56,8 @@ class HistoryGetResolver : DefaultGetResolver<History>() { | ||||
| class HistoryDeleteResolver : DefaultDeleteResolver<History>() { | ||||
|  | ||||
|     override fun mapToDeleteQuery(obj: History) = DeleteQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .where("$COL_ID = ?") | ||||
|             .whereArgs(obj.id) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .where("$COL_ID = ?") | ||||
|         .whereArgs(obj.id) | ||||
|         .build() | ||||
| } | ||||
|   | ||||
| @@ -16,22 +16,22 @@ import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.COL_MANGA_ID | ||||
| import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.TABLE | ||||
|  | ||||
| class MangaCategoryTypeMapping : SQLiteTypeMapping<MangaCategory>( | ||||
|         MangaCategoryPutResolver(), | ||||
|         MangaCategoryGetResolver(), | ||||
|         MangaCategoryDeleteResolver() | ||||
|     MangaCategoryPutResolver(), | ||||
|     MangaCategoryGetResolver(), | ||||
|     MangaCategoryDeleteResolver() | ||||
| ) | ||||
|  | ||||
| class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() { | ||||
|  | ||||
|     override fun mapToInsertQuery(obj: MangaCategory) = InsertQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .build() | ||||
|  | ||||
|     override fun mapToUpdateQuery(obj: MangaCategory) = UpdateQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .where("$COL_ID = ?") | ||||
|             .whereArgs(obj.id) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .where("$COL_ID = ?") | ||||
|         .whereArgs(obj.id) | ||||
|         .build() | ||||
|  | ||||
|     override fun mapToContentValues(obj: MangaCategory) = ContentValues(3).apply { | ||||
|         put(COL_ID, obj.id) | ||||
| @@ -52,8 +52,8 @@ class MangaCategoryGetResolver : DefaultGetResolver<MangaCategory>() { | ||||
| class MangaCategoryDeleteResolver : DefaultDeleteResolver<MangaCategory>() { | ||||
|  | ||||
|     override fun mapToDeleteQuery(obj: MangaCategory) = DeleteQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .where("$COL_ID = ?") | ||||
|             .whereArgs(obj.id) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .where("$COL_ID = ?") | ||||
|         .whereArgs(obj.id) | ||||
|         .build() | ||||
| } | ||||
|   | ||||
| @@ -29,22 +29,22 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_VIEWER | ||||
| import eu.kanade.tachiyomi.data.database.tables.MangaTable.TABLE | ||||
|  | ||||
| class MangaTypeMapping : SQLiteTypeMapping<Manga>( | ||||
|         MangaPutResolver(), | ||||
|         MangaGetResolver(), | ||||
|         MangaDeleteResolver() | ||||
|     MangaPutResolver(), | ||||
|     MangaGetResolver(), | ||||
|     MangaDeleteResolver() | ||||
| ) | ||||
|  | ||||
| class MangaPutResolver : DefaultPutResolver<Manga>() { | ||||
|  | ||||
|     override fun mapToInsertQuery(obj: Manga) = InsertQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .build() | ||||
|  | ||||
|     override fun mapToUpdateQuery(obj: Manga) = UpdateQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .where("$COL_ID = ?") | ||||
|             .whereArgs(obj.id) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .where("$COL_ID = ?") | ||||
|         .whereArgs(obj.id) | ||||
|         .build() | ||||
|  | ||||
|     override fun mapToContentValues(obj: Manga) = ContentValues(15).apply { | ||||
|         put(COL_ID, obj.id) | ||||
| @@ -95,8 +95,8 @@ open class MangaGetResolver : DefaultGetResolver<Manga>(), BaseMangaGetResolver | ||||
| class MangaDeleteResolver : DefaultDeleteResolver<Manga>() { | ||||
|  | ||||
|     override fun mapToDeleteQuery(obj: Manga) = DeleteQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .where("$COL_ID = ?") | ||||
|             .whereArgs(obj.id) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .where("$COL_ID = ?") | ||||
|         .whereArgs(obj.id) | ||||
|         .build() | ||||
| } | ||||
|   | ||||
| @@ -27,22 +27,22 @@ import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TRACKING_URL | ||||
| import eu.kanade.tachiyomi.data.database.tables.TrackTable.TABLE | ||||
|  | ||||
| class TrackTypeMapping : SQLiteTypeMapping<Track>( | ||||
|         TrackPutResolver(), | ||||
|         TrackGetResolver(), | ||||
|         TrackDeleteResolver() | ||||
|     TrackPutResolver(), | ||||
|     TrackGetResolver(), | ||||
|     TrackDeleteResolver() | ||||
| ) | ||||
|  | ||||
| class TrackPutResolver : DefaultPutResolver<Track>() { | ||||
|  | ||||
|     override fun mapToInsertQuery(obj: Track) = InsertQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .build() | ||||
|  | ||||
|     override fun mapToUpdateQuery(obj: Track) = UpdateQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .where("$COL_ID = ?") | ||||
|             .whereArgs(obj.id) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .where("$COL_ID = ?") | ||||
|         .whereArgs(obj.id) | ||||
|         .build() | ||||
|  | ||||
|     override fun mapToContentValues(obj: Track) = ContentValues(10).apply { | ||||
|         put(COL_ID, obj.id) | ||||
| @@ -83,8 +83,8 @@ class TrackGetResolver : DefaultGetResolver<Track>() { | ||||
| class TrackDeleteResolver : DefaultDeleteResolver<Track>() { | ||||
|  | ||||
|     override fun mapToDeleteQuery(obj: Track) = DeleteQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .where("$COL_ID = ?") | ||||
|             .whereArgs(obj.id) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .where("$COL_ID = ?") | ||||
|         .whereArgs(obj.id) | ||||
|         .build() | ||||
| } | ||||
|   | ||||
| @@ -10,20 +10,24 @@ import eu.kanade.tachiyomi.data.database.tables.CategoryTable | ||||
| interface CategoryQueries : DbProvider { | ||||
|  | ||||
|     fun getCategories() = db.get() | ||||
|             .listOfObjects(Category::class.java) | ||||
|             .withQuery(Query.builder() | ||||
|                     .table(CategoryTable.TABLE) | ||||
|                     .orderBy(CategoryTable.COL_ORDER) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .listOfObjects(Category::class.java) | ||||
|         .withQuery( | ||||
|             Query.builder() | ||||
|                 .table(CategoryTable.TABLE) | ||||
|                 .orderBy(CategoryTable.COL_ORDER) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getCategoriesForManga(manga: Manga) = db.get() | ||||
|             .listOfObjects(Category::class.java) | ||||
|             .withQuery(RawQuery.builder() | ||||
|                     .query(getCategoriesForMangaQuery()) | ||||
|                     .args(manga.id) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .listOfObjects(Category::class.java) | ||||
|         .withQuery( | ||||
|             RawQuery.builder() | ||||
|                 .query(getCategoriesForMangaQuery()) | ||||
|                 .args(manga.id) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun insertCategory(category: Category) = db.put().`object`(category).prepare() | ||||
|  | ||||
|   | ||||
| @@ -16,50 +16,60 @@ import java.util.Date | ||||
| interface ChapterQueries : DbProvider { | ||||
|  | ||||
|     fun getChapters(manga: Manga) = db.get() | ||||
|             .listOfObjects(Chapter::class.java) | ||||
|             .withQuery(Query.builder() | ||||
|                     .table(ChapterTable.TABLE) | ||||
|                     .where("${ChapterTable.COL_MANGA_ID} = ?") | ||||
|                     .whereArgs(manga.id) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .listOfObjects(Chapter::class.java) | ||||
|         .withQuery( | ||||
|             Query.builder() | ||||
|                 .table(ChapterTable.TABLE) | ||||
|                 .where("${ChapterTable.COL_MANGA_ID} = ?") | ||||
|                 .whereArgs(manga.id) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getRecentChapters(date: Date) = db.get() | ||||
|             .listOfObjects(MangaChapter::class.java) | ||||
|             .withQuery(RawQuery.builder() | ||||
|                     .query(getRecentsQuery()) | ||||
|                     .args(date.time) | ||||
|                     .observesTables(ChapterTable.TABLE) | ||||
|                     .build()) | ||||
|             .withGetResolver(MangaChapterGetResolver.INSTANCE) | ||||
|             .prepare() | ||||
|         .listOfObjects(MangaChapter::class.java) | ||||
|         .withQuery( | ||||
|             RawQuery.builder() | ||||
|                 .query(getRecentsQuery()) | ||||
|                 .args(date.time) | ||||
|                 .observesTables(ChapterTable.TABLE) | ||||
|                 .build() | ||||
|         ) | ||||
|         .withGetResolver(MangaChapterGetResolver.INSTANCE) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getChapter(id: Long) = db.get() | ||||
|             .`object`(Chapter::class.java) | ||||
|             .withQuery(Query.builder() | ||||
|                     .table(ChapterTable.TABLE) | ||||
|                     .where("${ChapterTable.COL_ID} = ?") | ||||
|                     .whereArgs(id) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .`object`(Chapter::class.java) | ||||
|         .withQuery( | ||||
|             Query.builder() | ||||
|                 .table(ChapterTable.TABLE) | ||||
|                 .where("${ChapterTable.COL_ID} = ?") | ||||
|                 .whereArgs(id) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getChapter(url: String) = db.get() | ||||
|             .`object`(Chapter::class.java) | ||||
|             .withQuery(Query.builder() | ||||
|                     .table(ChapterTable.TABLE) | ||||
|                     .where("${ChapterTable.COL_URL} = ?") | ||||
|                     .whereArgs(url) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .`object`(Chapter::class.java) | ||||
|         .withQuery( | ||||
|             Query.builder() | ||||
|                 .table(ChapterTable.TABLE) | ||||
|                 .where("${ChapterTable.COL_URL} = ?") | ||||
|                 .whereArgs(url) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getChapter(url: String, mangaId: Long) = db.get() | ||||
|             .`object`(Chapter::class.java) | ||||
|             .withQuery(Query.builder() | ||||
|                     .table(ChapterTable.TABLE) | ||||
|                     .where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?") | ||||
|                     .whereArgs(url, mangaId) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .`object`(Chapter::class.java) | ||||
|         .withQuery( | ||||
|             Query.builder() | ||||
|                 .table(ChapterTable.TABLE) | ||||
|                 .where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?") | ||||
|                 .whereArgs(url, mangaId) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare() | ||||
|  | ||||
| @@ -70,22 +80,22 @@ interface ChapterQueries : DbProvider { | ||||
|     fun deleteChapters(chapters: List<Chapter>) = db.delete().objects(chapters).prepare() | ||||
|  | ||||
|     fun updateChaptersBackup(chapters: List<Chapter>) = db.put() | ||||
|             .objects(chapters) | ||||
|             .withPutResolver(ChapterBackupPutResolver()) | ||||
|             .prepare() | ||||
|         .objects(chapters) | ||||
|         .withPutResolver(ChapterBackupPutResolver()) | ||||
|         .prepare() | ||||
|  | ||||
|     fun updateChapterProgress(chapter: Chapter) = db.put() | ||||
|             .`object`(chapter) | ||||
|             .withPutResolver(ChapterProgressPutResolver()) | ||||
|             .prepare() | ||||
|         .`object`(chapter) | ||||
|         .withPutResolver(ChapterProgressPutResolver()) | ||||
|         .prepare() | ||||
|  | ||||
|     fun updateChaptersProgress(chapters: List<Chapter>) = db.put() | ||||
|             .objects(chapters) | ||||
|             .withPutResolver(ChapterProgressPutResolver()) | ||||
|             .prepare() | ||||
|         .objects(chapters) | ||||
|         .withPutResolver(ChapterProgressPutResolver()) | ||||
|         .prepare() | ||||
|  | ||||
|     fun fixChaptersSourceOrder(chapters: List<Chapter>) = db.put() | ||||
|             .objects(chapters) | ||||
|             .withPutResolver(ChapterSourceOrderPutResolver()) | ||||
|             .prepare() | ||||
|         .objects(chapters) | ||||
|         .withPutResolver(ChapterSourceOrderPutResolver()) | ||||
|         .prepare() | ||||
| } | ||||
|   | ||||
| @@ -23,32 +23,38 @@ interface HistoryQueries : DbProvider { | ||||
|      * @param date recent date range | ||||
|      */ | ||||
|     fun getRecentManga(date: Date) = db.get() | ||||
|             .listOfObjects(MangaChapterHistory::class.java) | ||||
|             .withQuery(RawQuery.builder() | ||||
|                     .query(getRecentMangasQuery()) | ||||
|                     .args(date.time) | ||||
|                     .observesTables(HistoryTable.TABLE) | ||||
|                     .build()) | ||||
|             .withGetResolver(MangaChapterHistoryGetResolver.INSTANCE) | ||||
|             .prepare() | ||||
|         .listOfObjects(MangaChapterHistory::class.java) | ||||
|         .withQuery( | ||||
|             RawQuery.builder() | ||||
|                 .query(getRecentMangasQuery()) | ||||
|                 .args(date.time) | ||||
|                 .observesTables(HistoryTable.TABLE) | ||||
|                 .build() | ||||
|         ) | ||||
|         .withGetResolver(MangaChapterHistoryGetResolver.INSTANCE) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getHistoryByMangaId(mangaId: Long) = db.get() | ||||
|             .listOfObjects(History::class.java) | ||||
|             .withQuery(RawQuery.builder() | ||||
|                     .query(getHistoryByMangaId()) | ||||
|                     .args(mangaId) | ||||
|                     .observesTables(HistoryTable.TABLE) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .listOfObjects(History::class.java) | ||||
|         .withQuery( | ||||
|             RawQuery.builder() | ||||
|                 .query(getHistoryByMangaId()) | ||||
|                 .args(mangaId) | ||||
|                 .observesTables(HistoryTable.TABLE) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getHistoryByChapterUrl(chapterUrl: String) = db.get() | ||||
|             .`object`(History::class.java) | ||||
|             .withQuery(RawQuery.builder() | ||||
|                     .query(getHistoryByChapterUrl()) | ||||
|                     .args(chapterUrl) | ||||
|                     .observesTables(HistoryTable.TABLE) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .`object`(History::class.java) | ||||
|         .withQuery( | ||||
|             RawQuery.builder() | ||||
|                 .query(getHistoryByChapterUrl()) | ||||
|                 .args(chapterUrl) | ||||
|                 .observesTables(HistoryTable.TABLE) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     /** | ||||
|      * Updates the history last read. | ||||
| @@ -56,9 +62,9 @@ interface HistoryQueries : DbProvider { | ||||
|      * @param history history object | ||||
|      */ | ||||
|     fun updateHistoryLastRead(history: History) = db.put() | ||||
|             .`object`(history) | ||||
|             .withPutResolver(HistoryLastReadPutResolver()) | ||||
|             .prepare() | ||||
|         .`object`(history) | ||||
|         .withPutResolver(HistoryLastReadPutResolver()) | ||||
|         .prepare() | ||||
|  | ||||
|     /** | ||||
|      * Updates the history last read. | ||||
| @@ -66,21 +72,25 @@ interface HistoryQueries : DbProvider { | ||||
|      * @param historyList history object list | ||||
|      */ | ||||
|     fun updateHistoryLastRead(historyList: List<History>) = db.put() | ||||
|             .objects(historyList) | ||||
|             .withPutResolver(HistoryLastReadPutResolver()) | ||||
|             .prepare() | ||||
|         .objects(historyList) | ||||
|         .withPutResolver(HistoryLastReadPutResolver()) | ||||
|         .prepare() | ||||
|  | ||||
|     fun deleteHistory() = db.delete() | ||||
|             .byQuery(DeleteQuery.builder() | ||||
|                     .table(HistoryTable.TABLE) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .byQuery( | ||||
|             DeleteQuery.builder() | ||||
|                 .table(HistoryTable.TABLE) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun deleteHistoryNoLastRead() = db.delete() | ||||
|             .byQuery(DeleteQuery.builder() | ||||
|                     .table(HistoryTable.TABLE) | ||||
|                     .where("${HistoryTable.COL_LAST_READ} = ?") | ||||
|                     .whereArgs(0) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .byQuery( | ||||
|             DeleteQuery.builder() | ||||
|                 .table(HistoryTable.TABLE) | ||||
|                 .where("${HistoryTable.COL_LAST_READ} = ?") | ||||
|                 .whereArgs(0) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
| } | ||||
|   | ||||
| @@ -15,12 +15,14 @@ interface MangaCategoryQueries : DbProvider { | ||||
|     fun insertMangasCategories(mangasCategories: List<MangaCategory>) = db.put().objects(mangasCategories).prepare() | ||||
|  | ||||
|     fun deleteOldMangasCategories(mangas: List<Manga>) = db.delete() | ||||
|             .byQuery(DeleteQuery.builder() | ||||
|                     .table(MangaCategoryTable.TABLE) | ||||
|                     .where("${MangaCategoryTable.COL_MANGA_ID} IN (${Queries.placeholders(mangas.size)})") | ||||
|                     .whereArgs(*mangas.map { it.id }.toTypedArray()) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .byQuery( | ||||
|             DeleteQuery.builder() | ||||
|                 .table(MangaCategoryTable.TABLE) | ||||
|                 .where("${MangaCategoryTable.COL_MANGA_ID} IN (${Queries.placeholders(mangas.size)})") | ||||
|                 .whereArgs(*mangas.map { it.id }.toTypedArray()) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun setMangaCategories(mangasCategories: List<MangaCategory>, mangas: List<Manga>) { | ||||
|         db.inTransaction { | ||||
|   | ||||
| @@ -20,117 +20,137 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable | ||||
| interface MangaQueries : DbProvider { | ||||
|  | ||||
|     fun getMangas() = db.get() | ||||
|             .listOfObjects(Manga::class.java) | ||||
|             .withQuery(Query.builder() | ||||
|                     .table(MangaTable.TABLE) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .listOfObjects(Manga::class.java) | ||||
|         .withQuery( | ||||
|             Query.builder() | ||||
|                 .table(MangaTable.TABLE) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getLibraryMangas() = db.get() | ||||
|             .listOfObjects(LibraryManga::class.java) | ||||
|             .withQuery(RawQuery.builder() | ||||
|                     .query(libraryQuery) | ||||
|                     .observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE) | ||||
|                     .build()) | ||||
|             .withGetResolver(LibraryMangaGetResolver.INSTANCE) | ||||
|             .prepare() | ||||
|         .listOfObjects(LibraryManga::class.java) | ||||
|         .withQuery( | ||||
|             RawQuery.builder() | ||||
|                 .query(libraryQuery) | ||||
|                 .observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE) | ||||
|                 .build() | ||||
|         ) | ||||
|         .withGetResolver(LibraryMangaGetResolver.INSTANCE) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getFavoriteMangas() = db.get() | ||||
|             .listOfObjects(Manga::class.java) | ||||
|             .withQuery(Query.builder() | ||||
|                     .table(MangaTable.TABLE) | ||||
|                     .where("${MangaTable.COL_FAVORITE} = ?") | ||||
|                     .whereArgs(1) | ||||
|                     .orderBy(MangaTable.COL_TITLE) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .listOfObjects(Manga::class.java) | ||||
|         .withQuery( | ||||
|             Query.builder() | ||||
|                 .table(MangaTable.TABLE) | ||||
|                 .where("${MangaTable.COL_FAVORITE} = ?") | ||||
|                 .whereArgs(1) | ||||
|                 .orderBy(MangaTable.COL_TITLE) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getManga(url: String, sourceId: Long) = db.get() | ||||
|             .`object`(Manga::class.java) | ||||
|             .withQuery(Query.builder() | ||||
|                     .table(MangaTable.TABLE) | ||||
|                     .where("${MangaTable.COL_URL} = ? AND ${MangaTable.COL_SOURCE} = ?") | ||||
|                     .whereArgs(url, sourceId) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .`object`(Manga::class.java) | ||||
|         .withQuery( | ||||
|             Query.builder() | ||||
|                 .table(MangaTable.TABLE) | ||||
|                 .where("${MangaTable.COL_URL} = ? AND ${MangaTable.COL_SOURCE} = ?") | ||||
|                 .whereArgs(url, sourceId) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getManga(id: Long) = db.get() | ||||
|             .`object`(Manga::class.java) | ||||
|             .withQuery(Query.builder() | ||||
|                     .table(MangaTable.TABLE) | ||||
|                     .where("${MangaTable.COL_ID} = ?") | ||||
|                     .whereArgs(id) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .`object`(Manga::class.java) | ||||
|         .withQuery( | ||||
|             Query.builder() | ||||
|                 .table(MangaTable.TABLE) | ||||
|                 .where("${MangaTable.COL_ID} = ?") | ||||
|                 .whereArgs(id) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun insertManga(manga: Manga) = db.put().`object`(manga).prepare() | ||||
|  | ||||
|     fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare() | ||||
|  | ||||
|     fun updateFlags(manga: Manga) = db.put() | ||||
|             .`object`(manga) | ||||
|             .withPutResolver(MangaFlagsPutResolver()) | ||||
|             .prepare() | ||||
|         .`object`(manga) | ||||
|         .withPutResolver(MangaFlagsPutResolver()) | ||||
|         .prepare() | ||||
|  | ||||
|     fun updateLastUpdated(manga: Manga) = db.put() | ||||
|             .`object`(manga) | ||||
|             .withPutResolver(MangaLastUpdatedPutResolver()) | ||||
|             .prepare() | ||||
|         .`object`(manga) | ||||
|         .withPutResolver(MangaLastUpdatedPutResolver()) | ||||
|         .prepare() | ||||
|  | ||||
|     fun updateMangaFavorite(manga: Manga) = db.put() | ||||
|             .`object`(manga) | ||||
|             .withPutResolver(MangaFavoritePutResolver()) | ||||
|             .prepare() | ||||
|         .`object`(manga) | ||||
|         .withPutResolver(MangaFavoritePutResolver()) | ||||
|         .prepare() | ||||
|  | ||||
|     fun updateMangaViewer(manga: Manga) = db.put() | ||||
|             .`object`(manga) | ||||
|             .withPutResolver(MangaViewerPutResolver()) | ||||
|             .prepare() | ||||
|         .`object`(manga) | ||||
|         .withPutResolver(MangaViewerPutResolver()) | ||||
|         .prepare() | ||||
|  | ||||
|     fun updateMangaTitle(manga: Manga) = db.put() | ||||
|             .`object`(manga) | ||||
|             .withPutResolver(MangaTitlePutResolver()) | ||||
|             .prepare() | ||||
|         .`object`(manga) | ||||
|         .withPutResolver(MangaTitlePutResolver()) | ||||
|         .prepare() | ||||
|  | ||||
|     fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare() | ||||
|  | ||||
|     fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare() | ||||
|  | ||||
|     fun deleteMangasNotInLibrary() = db.delete() | ||||
|             .byQuery(DeleteQuery.builder() | ||||
|                     .table(MangaTable.TABLE) | ||||
|                     .where("${MangaTable.COL_FAVORITE} = ?") | ||||
|                     .whereArgs(0) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .byQuery( | ||||
|             DeleteQuery.builder() | ||||
|                 .table(MangaTable.TABLE) | ||||
|                 .where("${MangaTable.COL_FAVORITE} = ?") | ||||
|                 .whereArgs(0) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun deleteMangas() = db.delete() | ||||
|             .byQuery(DeleteQuery.builder() | ||||
|                     .table(MangaTable.TABLE) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .byQuery( | ||||
|             DeleteQuery.builder() | ||||
|                 .table(MangaTable.TABLE) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getLastReadManga() = db.get() | ||||
|             .listOfObjects(Manga::class.java) | ||||
|             .withQuery(RawQuery.builder() | ||||
|                     .query(getLastReadMangaQuery()) | ||||
|                     .observesTables(MangaTable.TABLE) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .listOfObjects(Manga::class.java) | ||||
|         .withQuery( | ||||
|             RawQuery.builder() | ||||
|                 .query(getLastReadMangaQuery()) | ||||
|                 .observesTables(MangaTable.TABLE) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getTotalChapterManga() = db.get() | ||||
|             .listOfObjects(Manga::class.java) | ||||
|             .withQuery(RawQuery.builder() | ||||
|                     .query(getTotalChapterMangaQuery()) | ||||
|                     .observesTables(MangaTable.TABLE) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .listOfObjects(Manga::class.java) | ||||
|         .withQuery( | ||||
|             RawQuery.builder() | ||||
|                 .query(getTotalChapterMangaQuery()) | ||||
|                 .observesTables(MangaTable.TABLE) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getLatestChapterManga() = db.get() | ||||
|             .listOfObjects(Manga::class.java) | ||||
|             .withQuery(RawQuery.builder() | ||||
|                     .query(getLatestChapterMangaQuery()) | ||||
|                     .observesTables(MangaTable.TABLE) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .listOfObjects(Manga::class.java) | ||||
|         .withQuery( | ||||
|             RawQuery.builder() | ||||
|                 .query(getLatestChapterMangaQuery()) | ||||
|                 .observesTables(MangaTable.TABLE) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
| } | ||||
|   | ||||
| @@ -9,7 +9,8 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga | ||||
| /** | ||||
|  * Query to get the manga from the library, with their categories and unread count. | ||||
|  */ | ||||
| val libraryQuery = """ | ||||
| val libraryQuery = | ||||
|     """ | ||||
|     SELECT M.*, COALESCE(MC.${MangaCategory.COL_CATEGORY_ID}, 0) AS ${Manga.COL_CATEGORY} | ||||
|     FROM ( | ||||
|         SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD} | ||||
| @@ -33,7 +34,8 @@ val libraryQuery = """ | ||||
| /** | ||||
|  * Query to get the recent chapters of manga from the library up to a date. | ||||
|  */ | ||||
| fun getRecentsQuery() = """ | ||||
| fun getRecentsQuery() = | ||||
|     """ | ||||
|     SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, * FROM ${Manga.TABLE} JOIN ${Chapter.TABLE} | ||||
|     ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} | ||||
|     WHERE ${Manga.COL_FAVORITE} = 1 AND ${Chapter.COL_DATE_UPLOAD} > ? | ||||
| @@ -47,7 +49,8 @@ fun getRecentsQuery() = """ | ||||
|  * and are read after the given time period | ||||
|  * @return return limit is 25 | ||||
|  */ | ||||
| fun getRecentMangasQuery() = """ | ||||
| fun getRecentMangasQuery() = | ||||
|     """ | ||||
|     SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, ${Manga.TABLE}.*, ${Chapter.TABLE}.*, ${History.TABLE}.* | ||||
|     FROM ${Manga.TABLE} | ||||
|     JOIN ${Chapter.TABLE} | ||||
| @@ -65,7 +68,8 @@ fun getRecentMangasQuery() = """ | ||||
|     LIMIT 25 | ||||
| """ | ||||
|  | ||||
| fun getHistoryByMangaId() = """ | ||||
| fun getHistoryByMangaId() = | ||||
|     """ | ||||
|     SELECT ${History.TABLE}.* | ||||
|     FROM ${History.TABLE} | ||||
|     JOIN ${Chapter.TABLE} | ||||
| @@ -73,7 +77,8 @@ fun getHistoryByMangaId() = """ | ||||
|     WHERE ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ? AND ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID} | ||||
| """ | ||||
|  | ||||
| fun getHistoryByChapterUrl() = """ | ||||
| fun getHistoryByChapterUrl() = | ||||
|     """ | ||||
|     SELECT ${History.TABLE}.* | ||||
|     FROM ${History.TABLE} | ||||
|     JOIN ${Chapter.TABLE} | ||||
| @@ -81,7 +86,8 @@ fun getHistoryByChapterUrl() = """ | ||||
|     WHERE ${Chapter.TABLE}.${Chapter.COL_URL} = ? AND ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID} | ||||
| """ | ||||
|  | ||||
| fun getLastReadMangaQuery() = """ | ||||
| fun getLastReadMangaQuery() = | ||||
|     """ | ||||
|     SELECT ${Manga.TABLE}.*, MAX(${History.TABLE}.${History.COL_LAST_READ}) AS max | ||||
|     FROM ${Manga.TABLE} | ||||
|     JOIN ${Chapter.TABLE} | ||||
| @@ -93,7 +99,8 @@ fun getLastReadMangaQuery() = """ | ||||
|     ORDER BY max DESC | ||||
| """ | ||||
|  | ||||
| fun getTotalChapterMangaQuery() = """ | ||||
| fun getTotalChapterMangaQuery() = | ||||
|     """ | ||||
|     SELECT ${Manga.TABLE}.* | ||||
|     FROM ${Manga.TABLE} | ||||
|     JOIN ${Chapter.TABLE} | ||||
| @@ -102,7 +109,8 @@ fun getTotalChapterMangaQuery() = """ | ||||
|     ORDER by COUNT(*) | ||||
| """ | ||||
|  | ||||
| fun getLatestChapterMangaQuery() = """ | ||||
| fun getLatestChapterMangaQuery() = | ||||
|     """ | ||||
|     SELECT ${Manga.TABLE}.*, MAX(${Chapter.TABLE}.${Chapter.COL_DATE_UPLOAD}) AS max | ||||
|     FROM ${Manga.TABLE} | ||||
|     JOIN ${Chapter.TABLE} | ||||
| @@ -114,7 +122,8 @@ fun getLatestChapterMangaQuery() = """ | ||||
| /** | ||||
|  * Query to get the categories for a manga. | ||||
|  */ | ||||
| fun getCategoriesForMangaQuery() = """ | ||||
| fun getCategoriesForMangaQuery() = | ||||
|     """ | ||||
|     SELECT ${Category.TABLE}.* FROM ${Category.TABLE} | ||||
|     JOIN ${MangaCategory.TABLE} ON ${Category.TABLE}.${Category.COL_ID} = | ||||
|     ${MangaCategory.TABLE}.${MangaCategory.COL_CATEGORY_ID} | ||||
|   | ||||
| @@ -11,23 +11,27 @@ import eu.kanade.tachiyomi.data.track.TrackService | ||||
| interface TrackQueries : DbProvider { | ||||
|  | ||||
|     fun getTracks(manga: Manga) = db.get() | ||||
|             .listOfObjects(Track::class.java) | ||||
|             .withQuery(Query.builder() | ||||
|                     .table(TrackTable.TABLE) | ||||
|                     .where("${TrackTable.COL_MANGA_ID} = ?") | ||||
|                     .whereArgs(manga.id) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .listOfObjects(Track::class.java) | ||||
|         .withQuery( | ||||
|             Query.builder() | ||||
|                 .table(TrackTable.TABLE) | ||||
|                 .where("${TrackTable.COL_MANGA_ID} = ?") | ||||
|                 .whereArgs(manga.id) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun insertTrack(track: Track) = db.put().`object`(track).prepare() | ||||
|  | ||||
|     fun insertTracks(tracks: List<Track>) = db.put().objects(tracks).prepare() | ||||
|  | ||||
|     fun deleteTrackForManga(manga: Manga, sync: TrackService) = db.delete() | ||||
|             .byQuery(DeleteQuery.builder() | ||||
|                     .table(TrackTable.TABLE) | ||||
|                     .where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?") | ||||
|                     .whereArgs(manga.id, sync.id) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .byQuery( | ||||
|             DeleteQuery.builder() | ||||
|                 .table(TrackTable.TABLE) | ||||
|                 .where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?") | ||||
|                 .whereArgs(manga.id, sync.id) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
| } | ||||
|   | ||||
| @@ -20,10 +20,10 @@ class ChapterBackupPutResolver : PutResolver<Chapter>() { | ||||
|     } | ||||
|  | ||||
|     fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder() | ||||
|             .table(ChapterTable.TABLE) | ||||
|             .where("${ChapterTable.COL_URL} = ?") | ||||
|             .whereArgs(chapter.url) | ||||
|             .build() | ||||
|         .table(ChapterTable.TABLE) | ||||
|         .where("${ChapterTable.COL_URL} = ?") | ||||
|         .whereArgs(chapter.url) | ||||
|         .build() | ||||
|  | ||||
|     fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply { | ||||
|         put(ChapterTable.COL_READ, chapter.read) | ||||
|   | ||||
| @@ -20,10 +20,10 @@ class ChapterProgressPutResolver : PutResolver<Chapter>() { | ||||
|     } | ||||
|  | ||||
|     fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder() | ||||
|             .table(ChapterTable.TABLE) | ||||
|             .where("${ChapterTable.COL_ID} = ?") | ||||
|             .whereArgs(chapter.id) | ||||
|             .build() | ||||
|         .table(ChapterTable.TABLE) | ||||
|         .where("${ChapterTable.COL_ID} = ?") | ||||
|         .whereArgs(chapter.id) | ||||
|         .build() | ||||
|  | ||||
|     fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply { | ||||
|         put(ChapterTable.COL_READ, chapter.read) | ||||
|   | ||||
| @@ -20,10 +20,10 @@ class ChapterSourceOrderPutResolver : PutResolver<Chapter>() { | ||||
|     } | ||||
|  | ||||
|     fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder() | ||||
|             .table(ChapterTable.TABLE) | ||||
|             .where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?") | ||||
|             .whereArgs(chapter.url, chapter.manga_id) | ||||
|             .build() | ||||
|         .table(ChapterTable.TABLE) | ||||
|         .where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?") | ||||
|         .whereArgs(chapter.url, chapter.manga_id) | ||||
|         .build() | ||||
|  | ||||
|     fun mapToContentValues(chapter: Chapter) = ContentValues(1).apply { | ||||
|         put(ChapterTable.COL_SOURCE_ORDER, chapter.source_order) | ||||
|   | ||||
| @@ -19,11 +19,13 @@ class HistoryLastReadPutResolver : HistoryPutResolver() { | ||||
|     override fun performPut(@NonNull db: StorIOSQLite, @NonNull history: History): PutResult = db.inTransactionReturn { | ||||
|         val updateQuery = mapToUpdateQuery(history) | ||||
|  | ||||
|         val cursor = db.lowLevel().query(Query.builder() | ||||
|         val cursor = db.lowLevel().query( | ||||
|             Query.builder() | ||||
|                 .table(updateQuery.table()) | ||||
|                 .where(updateQuery.where()) | ||||
|                 .whereArgs(updateQuery.whereArgs()) | ||||
|                 .build()) | ||||
|                 .build() | ||||
|         ) | ||||
|  | ||||
|         val putResult: PutResult | ||||
|  | ||||
| @@ -46,10 +48,10 @@ class HistoryLastReadPutResolver : HistoryPutResolver() { | ||||
|      * @param obj history object | ||||
|      */ | ||||
|     override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder() | ||||
|             .table(HistoryTable.TABLE) | ||||
|             .where("${HistoryTable.COL_CHAPTER_ID} = ?") | ||||
|             .whereArgs(obj.chapter_id) | ||||
|             .build() | ||||
|         .table(HistoryTable.TABLE) | ||||
|         .where("${HistoryTable.COL_CHAPTER_ID} = ?") | ||||
|         .whereArgs(obj.chapter_id) | ||||
|         .build() | ||||
|  | ||||
|     /** | ||||
|      * Create content query | ||||
|   | ||||
| @@ -20,10 +20,10 @@ class MangaFavoritePutResolver : PutResolver<Manga>() { | ||||
|     } | ||||
|  | ||||
|     fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder() | ||||
|             .table(MangaTable.TABLE) | ||||
|             .where("${MangaTable.COL_ID} = ?") | ||||
|             .whereArgs(manga.id) | ||||
|             .build() | ||||
|         .table(MangaTable.TABLE) | ||||
|         .where("${MangaTable.COL_ID} = ?") | ||||
|         .whereArgs(manga.id) | ||||
|         .build() | ||||
|  | ||||
|     fun mapToContentValues(manga: Manga) = ContentValues(1).apply { | ||||
|         put(MangaTable.COL_FAVORITE, manga.favorite) | ||||
|   | ||||
| @@ -20,10 +20,10 @@ class MangaFlagsPutResolver : PutResolver<Manga>() { | ||||
|     } | ||||
|  | ||||
|     fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder() | ||||
|             .table(MangaTable.TABLE) | ||||
|             .where("${MangaTable.COL_ID} = ?") | ||||
|             .whereArgs(manga.id) | ||||
|             .build() | ||||
|         .table(MangaTable.TABLE) | ||||
|         .where("${MangaTable.COL_ID} = ?") | ||||
|         .whereArgs(manga.id) | ||||
|         .build() | ||||
|  | ||||
|     fun mapToContentValues(manga: Manga) = ContentValues(1).apply { | ||||
|         put(MangaTable.COL_CHAPTER_FLAGS, manga.chapter_flags) | ||||
|   | ||||
| @@ -20,10 +20,10 @@ class MangaLastUpdatedPutResolver : PutResolver<Manga>() { | ||||
|     } | ||||
|  | ||||
|     fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder() | ||||
|             .table(MangaTable.TABLE) | ||||
|             .where("${MangaTable.COL_ID} = ?") | ||||
|             .whereArgs(manga.id) | ||||
|             .build() | ||||
|         .table(MangaTable.TABLE) | ||||
|         .where("${MangaTable.COL_ID} = ?") | ||||
|         .whereArgs(manga.id) | ||||
|         .build() | ||||
|  | ||||
|     fun mapToContentValues(manga: Manga) = ContentValues(1).apply { | ||||
|         put(MangaTable.COL_LAST_UPDATE, manga.last_update) | ||||
|   | ||||
| @@ -20,10 +20,10 @@ class MangaTitlePutResolver : PutResolver<Manga>() { | ||||
|     } | ||||
|  | ||||
|     fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder() | ||||
|             .table(MangaTable.TABLE) | ||||
|             .where("${MangaTable.COL_ID} = ?") | ||||
|             .whereArgs(manga.id) | ||||
|             .build() | ||||
|         .table(MangaTable.TABLE) | ||||
|         .where("${MangaTable.COL_ID} = ?") | ||||
|         .whereArgs(manga.id) | ||||
|         .build() | ||||
|  | ||||
|     fun mapToContentValues(manga: Manga) = ContentValues(1).apply { | ||||
|         put(MangaTable.COL_TITLE, manga.title) | ||||
|   | ||||
| @@ -20,10 +20,10 @@ class MangaViewerPutResolver : PutResolver<Manga>() { | ||||
|     } | ||||
|  | ||||
|     fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder() | ||||
|             .table(MangaTable.TABLE) | ||||
|             .where("${MangaTable.COL_ID} = ?") | ||||
|             .whereArgs(manga.id) | ||||
|             .build() | ||||
|         .table(MangaTable.TABLE) | ||||
|         .where("${MangaTable.COL_ID} = ?") | ||||
|         .whereArgs(manga.id) | ||||
|         .build() | ||||
|  | ||||
|     fun mapToContentValues(manga: Manga) = ContentValues(1).apply { | ||||
|         put(MangaTable.COL_VIEWER, manga.viewer) | ||||
|   | ||||
| @@ -13,7 +13,8 @@ object CategoryTable { | ||||
|     const val COL_FLAGS = "flags" | ||||
|  | ||||
|     val createTableQuery: String | ||||
|         get() = """CREATE TABLE $TABLE( | ||||
|         get() = | ||||
|             """CREATE TABLE $TABLE( | ||||
|             $COL_ID INTEGER NOT NULL PRIMARY KEY, | ||||
|             $COL_NAME TEXT NOT NULL, | ||||
|             $COL_ORDER INTEGER NOT NULL, | ||||
|   | ||||
| @@ -29,7 +29,8 @@ object ChapterTable { | ||||
|     const val COL_SOURCE_ORDER = "source_order" | ||||
|  | ||||
|     val createTableQuery: String | ||||
|         get() = """CREATE TABLE $TABLE( | ||||
|         get() = | ||||
|             """CREATE TABLE $TABLE( | ||||
|             $COL_ID INTEGER NOT NULL PRIMARY KEY, | ||||
|             $COL_MANGA_ID INTEGER NOT NULL, | ||||
|             $COL_URL TEXT NOT NULL, | ||||
| @@ -51,7 +52,7 @@ object ChapterTable { | ||||
|  | ||||
|     val createUnreadChaptersIndexQuery: String | ||||
|         get() = "CREATE INDEX ${TABLE}_unread_by_manga_index ON $TABLE($COL_MANGA_ID, $COL_READ) " + | ||||
|                 "WHERE $COL_READ = 0" | ||||
|             "WHERE $COL_READ = 0" | ||||
|  | ||||
|     val sourceOrderUpdateQuery: String | ||||
|         get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SOURCE_ORDER INTEGER DEFAULT 0" | ||||
|   | ||||
| @@ -31,7 +31,8 @@ object HistoryTable { | ||||
|      * query to create history table | ||||
|      */ | ||||
|     val createTableQuery: String | ||||
|         get() = """CREATE TABLE $TABLE( | ||||
|         get() = | ||||
|             """CREATE TABLE $TABLE( | ||||
|             $COL_ID INTEGER NOT NULL PRIMARY KEY, | ||||
|             $COL_CHAPTER_ID INTEGER NOT NULL UNIQUE, | ||||
|             $COL_LAST_READ LONG, | ||||
|   | ||||
| @@ -11,7 +11,8 @@ object MangaCategoryTable { | ||||
|     const val COL_CATEGORY_ID = "category_id" | ||||
|  | ||||
|     val createTableQuery: String | ||||
|         get() = """CREATE TABLE $TABLE( | ||||
|         get() = | ||||
|             """CREATE TABLE $TABLE( | ||||
|             $COL_ID INTEGER NOT NULL PRIMARY KEY, | ||||
|             $COL_MANGA_ID INTEGER NOT NULL, | ||||
|             $COL_CATEGORY_ID INTEGER NOT NULL, | ||||
|   | ||||
| @@ -39,7 +39,8 @@ object MangaTable { | ||||
|     const val COL_CATEGORY = "category" | ||||
|  | ||||
|     val createTableQuery: String | ||||
|         get() = """CREATE TABLE $TABLE( | ||||
|         get() = | ||||
|             """CREATE TABLE $TABLE( | ||||
|             $COL_ID INTEGER NOT NULL PRIMARY KEY, | ||||
|             $COL_SOURCE INTEGER NOT NULL, | ||||
|             $COL_URL TEXT NOT NULL, | ||||
| @@ -62,5 +63,5 @@ object MangaTable { | ||||
|  | ||||
|     val createLibraryIndexQuery: String | ||||
|         get() = "CREATE INDEX library_${COL_FAVORITE}_index ON $TABLE($COL_FAVORITE) " + | ||||
|                 "WHERE $COL_FAVORITE = 1" | ||||
|             "WHERE $COL_FAVORITE = 1" | ||||
| } | ||||
|   | ||||
| @@ -31,7 +31,8 @@ object TrackTable { | ||||
|     const val COL_FINISH_DATE = "finish_date" | ||||
|  | ||||
|     val createTableQuery: String | ||||
|         get() = """CREATE TABLE $TABLE( | ||||
|         get() = | ||||
|             """CREATE TABLE $TABLE( | ||||
|             $COL_ID INTEGER NOT NULL PRIMARY KEY, | ||||
|             $COL_MANGA_ID INTEGER NOT NULL, | ||||
|             $COL_SYNC_ID INTEGER NOT NULL, | ||||
|   | ||||
| @@ -100,8 +100,8 @@ class DownloadCache( | ||||
|             val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] | ||||
|             if (mangaDir != null) { | ||||
|                 return mangaDir.files | ||||
|                         .filter { !it.endsWith(Downloader.TMP_DIR_SUFFIX) } | ||||
|                         .size | ||||
|                     .filter { !it.endsWith(Downloader.TMP_DIR_SUFFIX) } | ||||
|                     .size | ||||
|             } | ||||
|         } | ||||
|         return 0 | ||||
| @@ -125,26 +125,26 @@ class DownloadCache( | ||||
|         val onlineSources = sourceManager.getOnlineSources() | ||||
|  | ||||
|         val sourceDirs = rootDir.dir.listFiles() | ||||
|                 .orEmpty() | ||||
|                 .associate { it.name to SourceDirectory(it) } | ||||
|                 .mapNotNullKeys { entry -> | ||||
|                     onlineSources.find { provider.getSourceDirName(it) == entry.key }?.id | ||||
|                 } | ||||
|             .orEmpty() | ||||
|             .associate { it.name to SourceDirectory(it) } | ||||
|             .mapNotNullKeys { entry -> | ||||
|                 onlineSources.find { provider.getSourceDirName(it) == entry.key }?.id | ||||
|             } | ||||
|  | ||||
|         rootDir.files = sourceDirs | ||||
|  | ||||
|         sourceDirs.values.forEach { sourceDir -> | ||||
|             val mangaDirs = sourceDir.dir.listFiles() | ||||
|                     .orEmpty() | ||||
|                     .associateNotNullKeys { it.name to MangaDirectory(it) } | ||||
|                 .orEmpty() | ||||
|                 .associateNotNullKeys { it.name to MangaDirectory(it) } | ||||
|  | ||||
|             sourceDir.files = mangaDirs | ||||
|  | ||||
|             mangaDirs.values.forEach { mangaDir -> | ||||
|                 val chapterDirs = mangaDir.dir.listFiles() | ||||
|                         .orEmpty() | ||||
|                         .mapNotNull { it.name } | ||||
|                         .toHashSet() | ||||
|                     .orEmpty() | ||||
|                     .mapNotNull { it.name } | ||||
|                     .toHashSet() | ||||
|  | ||||
|                 mangaDir.files = chapterDirs | ||||
|             } | ||||
|   | ||||
| @@ -148,16 +148,16 @@ class DownloadManager(private val context: Context) { | ||||
|     private fun buildPageList(chapterDir: UniFile?): Observable<List<Page>> { | ||||
|         return Observable.fromCallable { | ||||
|             val files = chapterDir?.listFiles().orEmpty() | ||||
|                     .filter { "image" in it.type.orEmpty() } | ||||
|                 .filter { "image" in it.type.orEmpty() } | ||||
|  | ||||
|             if (files.isEmpty()) { | ||||
|                 throw Exception("Page list is empty") | ||||
|             } | ||||
|  | ||||
|             files.sortedBy { it.name } | ||||
|                     .mapIndexed { i, file -> | ||||
|                         Page(i, uri = file.uri).apply { status = Page.READY } | ||||
|                     } | ||||
|                 .mapIndexed { i, file -> | ||||
|                     Page(i, uri = file.uri).apply { status = Page.READY } | ||||
|                 } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -87,13 +87,15 @@ internal class DownloadNotifier(private val context: Context) { | ||||
|                 setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) | ||||
|                 isDownloading = true | ||||
|                 // Pause action | ||||
|                 addAction(R.drawable.ic_pause_24dp, | ||||
|                         context.getString(R.string.action_pause), | ||||
|                         NotificationReceiver.pauseDownloadsPendingBroadcast(context)) | ||||
|                 addAction( | ||||
|                     R.drawable.ic_pause_24dp, | ||||
|                     context.getString(R.string.action_pause), | ||||
|                     NotificationReceiver.pauseDownloadsPendingBroadcast(context) | ||||
|                 ) | ||||
|             } | ||||
|  | ||||
|             val downloadingProgressText = context.getString(R.string.chapter_downloading_progress) | ||||
|                     .format(download.downloadedImages, download.pages!!.size) | ||||
|                 .format(download.downloadedImages, download.pages!!.size) | ||||
|  | ||||
|             if (preferences.hideNotificationContent()) { | ||||
|                 setContentTitle(downloadingProgressText) | ||||
| @@ -126,13 +128,17 @@ internal class DownloadNotifier(private val context: Context) { | ||||
|             // Open download manager when clicked | ||||
|             setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) | ||||
|             // Resume action | ||||
|             addAction(R.drawable.ic_play_arrow_24dp, | ||||
|                     context.getString(R.string.action_resume), | ||||
|                     NotificationReceiver.resumeDownloadsPendingBroadcast(context)) | ||||
|             addAction( | ||||
|                 R.drawable.ic_play_arrow_24dp, | ||||
|                 context.getString(R.string.action_resume), | ||||
|                 NotificationReceiver.resumeDownloadsPendingBroadcast(context) | ||||
|             ) | ||||
|             // Clear action | ||||
|             addAction(R.drawable.ic_close_24dp, | ||||
|                     context.getString(R.string.action_cancel_all), | ||||
|                     NotificationReceiver.clearDownloadsPendingBroadcast(context)) | ||||
|             addAction( | ||||
|                 R.drawable.ic_close_24dp, | ||||
|                 context.getString(R.string.action_cancel_all), | ||||
|                 NotificationReceiver.clearDownloadsPendingBroadcast(context) | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|         // Show notification. | ||||
| @@ -173,8 +179,10 @@ internal class DownloadNotifier(private val context: Context) { | ||||
|     fun onError(error: String? = null, chapter: String? = null) { | ||||
|         // Create notification | ||||
|         with(notificationBuilder) { | ||||
|             setContentTitle(chapter | ||||
|                     ?: context.getString(R.string.download_notifier_downloader_title)) | ||||
|             setContentTitle( | ||||
|                 chapter | ||||
|                     ?: context.getString(R.string.download_notifier_downloader_title) | ||||
|             ) | ||||
|             setContentText(error ?: context.getString(R.string.download_notifier_unknown_error)) | ||||
|             setSmallIcon(android.R.drawable.stat_sys_warning) | ||||
|             clearActions() | ||||
|   | ||||
| @@ -52,8 +52,8 @@ class DownloadProvider(private val context: Context) { | ||||
|     internal fun getMangaDir(manga: Manga, source: Source): UniFile { | ||||
|         try { | ||||
|             return downloadsDir | ||||
|                     .createDirectory(getSourceDirName(source)) | ||||
|                     .createDirectory(getMangaDirName(manga)) | ||||
|                 .createDirectory(getSourceDirName(source)) | ||||
|                 .createDirectory(getMangaDirName(manga)) | ||||
|         } catch (e: NullPointerException) { | ||||
|             throw Exception(context.getString(R.string.invalid_download_dir)) | ||||
|         } | ||||
|   | ||||
| @@ -123,14 +123,17 @@ class DownloadService : Service() { | ||||
|      */ | ||||
|     private fun listenNetworkChanges() { | ||||
|         subscriptions += ReactiveNetwork.observeNetworkConnectivity(applicationContext) | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe({ state -> | ||||
|             .subscribeOn(Schedulers.io()) | ||||
|             .observeOn(AndroidSchedulers.mainThread()) | ||||
|             .subscribe( | ||||
|                 { state -> | ||||
|                     onNetworkStateChanged(state) | ||||
|                 }, { | ||||
|                 }, | ||||
|                 { | ||||
|                     toast(R.string.download_queue_error) | ||||
|                     stopSelf() | ||||
|                 }) | ||||
|                 } | ||||
|             ) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -162,10 +165,11 @@ class DownloadService : Service() { | ||||
|      */ | ||||
|     private fun listenDownloaderState() { | ||||
|         subscriptions += downloadManager.runningRelay.subscribe { running -> | ||||
|             if (running) | ||||
|             if (running) { | ||||
|                 wakeLock.acquireIfNeeded() | ||||
|             else | ||||
|             } else { | ||||
|                 wakeLock.releaseIfNeeded() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -77,9 +77,9 @@ class DownloadStore( | ||||
|      */ | ||||
|     fun restore(): List<Download> { | ||||
|         val objs = preferences.all | ||||
|                 .mapNotNull { it.value as? String } | ||||
|                 .mapNotNull { deserialize(it) } | ||||
|                 .sortedBy { it.order } | ||||
|             .mapNotNull { it.value as? String } | ||||
|             .mapNotNull { deserialize(it) } | ||||
|             .sortedBy { it.order } | ||||
|  | ||||
|         val downloads = mutableListOf<Download>() | ||||
|         if (objs.isNotEmpty()) { | ||||
|   | ||||
| @@ -100,11 +100,13 @@ class Downloader( | ||||
|      * @return true if the downloader is started, false otherwise. | ||||
|      */ | ||||
|     fun start(): Boolean { | ||||
|         if (isRunning || queue.isEmpty()) | ||||
|         if (isRunning || queue.isEmpty()) { | ||||
|             return false | ||||
|         } | ||||
|  | ||||
|         if (!subscriptions.hasSubscriptions()) | ||||
|         if (!subscriptions.hasSubscriptions()) { | ||||
|             initializeSubscriptions() | ||||
|         } | ||||
|  | ||||
|         val pending = queue.filter { it.status != Download.DOWNLOADED } | ||||
|         pending.forEach { if (it.status != Download.QUEUE) it.status = Download.QUEUE } | ||||
| @@ -119,8 +121,8 @@ class Downloader( | ||||
|     fun stop(reason: String? = null) { | ||||
|         destroySubscriptions() | ||||
|         queue | ||||
|                 .filter { it.status == Download.DOWNLOADING } | ||||
|                 .forEach { it.status = Download.ERROR } | ||||
|             .filter { it.status == Download.DOWNLOADING } | ||||
|             .forEach { it.status = Download.ERROR } | ||||
|  | ||||
|         if (reason != null) { | ||||
|             notifier.onWarning(reason) | ||||
| @@ -140,8 +142,8 @@ class Downloader( | ||||
|     fun pause() { | ||||
|         destroySubscriptions() | ||||
|         queue | ||||
|                 .filter { it.status == Download.DOWNLOADING } | ||||
|                 .forEach { it.status = Download.QUEUE } | ||||
|             .filter { it.status == Download.DOWNLOADING } | ||||
|             .forEach { it.status = Download.QUEUE } | ||||
|         notifier.paused = true | ||||
|     } | ||||
|  | ||||
| @@ -156,8 +158,8 @@ class Downloader( | ||||
|         // Needed to update the chapter view | ||||
|         if (isNotification) { | ||||
|             queue | ||||
|                     .filter { it.status == Download.QUEUE } | ||||
|                     .forEach { it.status = Download.NOT_DOWNLOADED } | ||||
|                 .filter { it.status == Download.QUEUE } | ||||
|                 .forEach { it.status = Download.NOT_DOWNLOADED } | ||||
|         } | ||||
|         queue.clear() | ||||
|         notifier.dismiss() | ||||
| @@ -174,16 +176,19 @@ class Downloader( | ||||
|         subscriptions.clear() | ||||
|  | ||||
|         subscriptions += downloadsRelay.concatMapIterable { it } | ||||
|                 .concatMap { downloadChapter(it).subscribeOn(Schedulers.io()) } | ||||
|                 .onBackpressureBuffer() | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe({ | ||||
|             .concatMap { downloadChapter(it).subscribeOn(Schedulers.io()) } | ||||
|             .onBackpressureBuffer() | ||||
|             .observeOn(AndroidSchedulers.mainThread()) | ||||
|             .subscribe( | ||||
|                 { | ||||
|                     completeDownload(it) | ||||
|                 }, { error -> | ||||
|                 }, | ||||
|                 { error -> | ||||
|                     DownloadService.stop(context) | ||||
|                     Timber.e(error) | ||||
|                     notifier.onError(error.message) | ||||
|                 }) | ||||
|                 } | ||||
|             ) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -212,20 +217,20 @@ class Downloader( | ||||
|             val mangaDir = provider.findMangaDir(manga, source) | ||||
|  | ||||
|             chapters | ||||
|                     // Avoid downloading chapters with the same name. | ||||
|                     .distinctBy { it.name } | ||||
|                     // Filter out those already downloaded. | ||||
|                     .filter { mangaDir?.findFile(provider.getChapterDirName(it)) == null } | ||||
|                     // Add chapters to queue from the start. | ||||
|                     .sortedByDescending { it.source_order } | ||||
|                 // Avoid downloading chapters with the same name. | ||||
|                 .distinctBy { it.name } | ||||
|                 // Filter out those already downloaded. | ||||
|                 .filter { mangaDir?.findFile(provider.getChapterDirName(it)) == null } | ||||
|                 // Add chapters to queue from the start. | ||||
|                 .sortedByDescending { it.source_order } | ||||
|         } | ||||
|  | ||||
|         // Runs in main thread (synchronization needed). | ||||
|         val chaptersToQueue = chaptersWithoutDir.await() | ||||
|                 // Filter out those already enqueued. | ||||
|                 .filter { chapter -> queue.none { it.chapter.id == chapter.id } } | ||||
|                 // Create a download for each one. | ||||
|                 .map { Download(source, manga, it) } | ||||
|             // Filter out those already enqueued. | ||||
|             .filter { chapter -> queue.none { it.chapter.id == chapter.id } } | ||||
|             // Create a download for each one. | ||||
|             .map { Download(source, manga, it) } | ||||
|  | ||||
|         if (chaptersToQueue.isNotEmpty()) { | ||||
|             queue.addAll(chaptersToQueue) | ||||
| @@ -255,43 +260,43 @@ class Downloader( | ||||
|         val pageListObservable = if (download.pages == null) { | ||||
|             // Pull page list from network and add them to download object | ||||
|             download.source.fetchPageList(download.chapter) | ||||
|                     .doOnNext { pages -> | ||||
|                         if (pages.isEmpty()) { | ||||
|                             throw Exception("Page list is empty") | ||||
|                         } | ||||
|                         download.pages = pages | ||||
|                 .doOnNext { pages -> | ||||
|                     if (pages.isEmpty()) { | ||||
|                         throw Exception("Page list is empty") | ||||
|                     } | ||||
|                     download.pages = pages | ||||
|                 } | ||||
|         } else { | ||||
|             // Or if the page list already exists, start from the file | ||||
|             Observable.just(download.pages!!) | ||||
|         } | ||||
|  | ||||
|         pageListObservable | ||||
|                 .doOnNext { _ -> | ||||
|                     // Delete all temporary (unfinished) files | ||||
|                     tmpDir.listFiles() | ||||
|                             ?.filter { it.name!!.endsWith(".tmp") } | ||||
|                             ?.forEach { it.delete() } | ||||
|             .doOnNext { _ -> | ||||
|                 // Delete all temporary (unfinished) files | ||||
|                 tmpDir.listFiles() | ||||
|                     ?.filter { it.name!!.endsWith(".tmp") } | ||||
|                     ?.forEach { it.delete() } | ||||
|  | ||||
|                     download.downloadedImages = 0 | ||||
|                     download.status = Download.DOWNLOADING | ||||
|                 } | ||||
|                 // Get all the URLs to the source images, fetch pages if necessary | ||||
|                 .flatMap { download.source.fetchAllImageUrlsFromPageList(it) } | ||||
|                 // Start downloading images, consider we can have downloaded images already | ||||
|                 .concatMap { page -> getOrDownloadImage(page, download, tmpDir) } | ||||
|                 // Do when page is downloaded. | ||||
|                 .doOnNext { notifier.onProgressChange(download) } | ||||
|                 .toList() | ||||
|                 .map { download } | ||||
|                 // Do after download completes | ||||
|                 .doOnNext { ensureSuccessfulDownload(download, mangaDir, tmpDir, chapterDirname) } | ||||
|                 // If the page list threw, it will resume here | ||||
|                 .onErrorReturn { error -> | ||||
|                     download.status = Download.ERROR | ||||
|                     notifier.onError(error.message, download.chapter.name) | ||||
|                     download | ||||
|                 } | ||||
|                 download.downloadedImages = 0 | ||||
|                 download.status = Download.DOWNLOADING | ||||
|             } | ||||
|             // Get all the URLs to the source images, fetch pages if necessary | ||||
|             .flatMap { download.source.fetchAllImageUrlsFromPageList(it) } | ||||
|             // Start downloading images, consider we can have downloaded images already | ||||
|             .concatMap { page -> getOrDownloadImage(page, download, tmpDir) } | ||||
|             // Do when page is downloaded. | ||||
|             .doOnNext { notifier.onProgressChange(download) } | ||||
|             .toList() | ||||
|             .map { download } | ||||
|             // Do after download completes | ||||
|             .doOnNext { ensureSuccessfulDownload(download, mangaDir, tmpDir, chapterDirname) } | ||||
|             // If the page list threw, it will resume here | ||||
|             .onErrorReturn { error -> | ||||
|                 download.status = Download.ERROR | ||||
|                 notifier.onError(error.message, download.chapter.name) | ||||
|                 download | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -304,8 +309,9 @@ class Downloader( | ||||
|      */ | ||||
|     private fun getOrDownloadImage(page: Page, download: Download, tmpDir: UniFile): Observable<Page> { | ||||
|         // If the image URL is empty, do nothing | ||||
|         if (page.imageUrl == null) | ||||
|         if (page.imageUrl == null) { | ||||
|             return Observable.just(page) | ||||
|         } | ||||
|  | ||||
|         val filename = String.format("%03d", page.number) | ||||
|         val tmpFile = tmpDir.findFile("$filename.tmp") | ||||
| @@ -317,26 +323,27 @@ class Downloader( | ||||
|         val imageFile = tmpDir.listFiles()!!.find { it.name!!.startsWith("$filename.") } | ||||
|  | ||||
|         // If the image is already downloaded, do nothing. Otherwise download from network | ||||
|         val pageObservable = if (imageFile != null) | ||||
|         val pageObservable = if (imageFile != null) { | ||||
|             Observable.just(imageFile) | ||||
|         else | ||||
|         } else { | ||||
|             downloadImage(page, download.source, tmpDir, filename) | ||||
|         } | ||||
|  | ||||
|         return pageObservable | ||||
|                 // When the image is ready, set image path, progress (just in case) and status | ||||
|                 .doOnNext { file -> | ||||
|                     page.uri = file.uri | ||||
|                     page.progress = 100 | ||||
|                     download.downloadedImages++ | ||||
|                     page.status = Page.READY | ||||
|                 } | ||||
|                 .map { page } | ||||
|                 // Mark this page as error and allow to download the remaining | ||||
|                 .onErrorReturn { | ||||
|                     page.progress = 0 | ||||
|                     page.status = Page.ERROR | ||||
|                     page | ||||
|                 } | ||||
|             // When the image is ready, set image path, progress (just in case) and status | ||||
|             .doOnNext { file -> | ||||
|                 page.uri = file.uri | ||||
|                 page.progress = 100 | ||||
|                 download.downloadedImages++ | ||||
|                 page.status = Page.READY | ||||
|             } | ||||
|             .map { page } | ||||
|             // Mark this page as error and allow to download the remaining | ||||
|             .onErrorReturn { | ||||
|                 page.progress = 0 | ||||
|                 page.status = Page.ERROR | ||||
|                 page | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -351,21 +358,21 @@ class Downloader( | ||||
|         page.status = Page.DOWNLOAD_IMAGE | ||||
|         page.progress = 0 | ||||
|         return source.fetchImage(page) | ||||
|                 .map { response -> | ||||
|                     val file = tmpDir.createFile("$filename.tmp") | ||||
|                     try { | ||||
|                         response.body!!.source().saveTo(file.openOutputStream()) | ||||
|                         val extension = getImageExtension(response, file) | ||||
|                         file.renameTo("$filename.$extension") | ||||
|                     } catch (e: Exception) { | ||||
|                         response.close() | ||||
|                         file.delete() | ||||
|                         throw e | ||||
|                     } | ||||
|                     file | ||||
|             .map { response -> | ||||
|                 val file = tmpDir.createFile("$filename.tmp") | ||||
|                 try { | ||||
|                     response.body!!.source().saveTo(file.openOutputStream()) | ||||
|                     val extension = getImageExtension(response, file) | ||||
|                     file.renameTo("$filename.$extension") | ||||
|                 } catch (e: Exception) { | ||||
|                     response.close() | ||||
|                     file.delete() | ||||
|                     throw e | ||||
|                 } | ||||
|                 // Retry 3 times, waiting 2, 4 and 8 seconds between attempts. | ||||
|                 .retryWhen(RetryWithDelay(3, { (2 shl it - 1) * 1000 }, Schedulers.trampoline())) | ||||
|                 file | ||||
|             } | ||||
|             // Retry 3 times, waiting 2, 4 and 8 seconds between attempts. | ||||
|             .retryWhen(RetryWithDelay(3, { (2 shl it - 1) * 1000 }, Schedulers.trampoline())) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -378,10 +385,10 @@ class Downloader( | ||||
|     private fun getImageExtension(response: Response, file: UniFile): String { | ||||
|         // Read content type if available. | ||||
|         val mime = response.body?.contentType()?.let { ct -> "${ct.type}/${ct.subtype}" } | ||||
|                 // Else guess from the uri. | ||||
|                 ?: context.contentResolver.getType(file.uri) | ||||
|                 // Else read magic numbers. | ||||
|                 ?: ImageUtil.findImageType { file.openInputStream() }?.mime | ||||
|             // Else guess from the uri. | ||||
|             ?: context.contentResolver.getType(file.uri) | ||||
|             // Else read magic numbers. | ||||
|             ?: ImageUtil.findImageType { file.openInputStream() }?.mime | ||||
|  | ||||
|         return MimeTypeMap.getSingleton().getExtensionFromMimeType(mime) ?: "jpg" | ||||
|     } | ||||
| @@ -400,7 +407,6 @@ class Downloader( | ||||
|         tmpDir: UniFile, | ||||
|         dirname: String | ||||
|     ) { | ||||
|  | ||||
|         // Ensure that the chapter folder has all the images. | ||||
|         val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") } | ||||
|  | ||||
|   | ||||
| @@ -70,13 +70,13 @@ class DownloadQueue( | ||||
|     } | ||||
|  | ||||
|     fun getActiveDownloads(): Observable<Download> = | ||||
|             Observable.from(this).filter { download -> download.status == Download.DOWNLOADING } | ||||
|         Observable.from(this).filter { download -> download.status == Download.DOWNLOADING } | ||||
|  | ||||
|     fun getStatusObservable(): Observable<Download> = statusSubject.onBackpressureBuffer() | ||||
|  | ||||
|     fun getUpdatedObservable(): Observable<List<Download>> = updatedRelay.onBackpressureBuffer() | ||||
|             .startWith(Unit) | ||||
|             .map { this } | ||||
|         .startWith(Unit) | ||||
|         .map { this } | ||||
|  | ||||
|     private fun setPagesFor(download: Download) { | ||||
|         if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) { | ||||
| @@ -86,21 +86,21 @@ class DownloadQueue( | ||||
|  | ||||
|     fun getProgressObservable(): Observable<Download> { | ||||
|         return statusSubject.onBackpressureBuffer() | ||||
|                 .startWith(getActiveDownloads()) | ||||
|                 .flatMap { download -> | ||||
|                     if (download.status == Download.DOWNLOADING) { | ||||
|                         val pageStatusSubject = PublishSubject.create<Int>() | ||||
|                         setPagesSubject(download.pages, pageStatusSubject) | ||||
|                         return@flatMap pageStatusSubject | ||||
|                                 .onBackpressureBuffer() | ||||
|                                 .filter { it == Page.READY } | ||||
|                                 .map { download } | ||||
|                     } else if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) { | ||||
|                         setPagesSubject(download.pages, null) | ||||
|                     } | ||||
|                     Observable.just(download) | ||||
|             .startWith(getActiveDownloads()) | ||||
|             .flatMap { download -> | ||||
|                 if (download.status == Download.DOWNLOADING) { | ||||
|                     val pageStatusSubject = PublishSubject.create<Int>() | ||||
|                     setPagesSubject(download.pages, pageStatusSubject) | ||||
|                     return@flatMap pageStatusSubject | ||||
|                         .onBackpressureBuffer() | ||||
|                         .filter { it == Page.READY } | ||||
|                         .map { download } | ||||
|                 } else if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) { | ||||
|                     setPagesSubject(download.pages, null) | ||||
|                 } | ||||
|                 .filter { it.status == Download.DOWNLOADING } | ||||
|                 Observable.just(download) | ||||
|             } | ||||
|             .filter { it.status == Download.DOWNLOADING } | ||||
|     } | ||||
|  | ||||
|     private fun setPagesSubject(pages: List<Page>?, subject: PublishSubject<Int>?) { | ||||
|   | ||||
| @@ -25,36 +25,39 @@ class LibraryMangaUrlFetcher( | ||||
|  | ||||
|     override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) { | ||||
|         if (!file.exists()) { | ||||
|             networkFetcher.loadData(priority, object : DataFetcher.DataCallback<InputStream> { | ||||
|                 override fun onDataReady(data: InputStream?) { | ||||
|                     if (data != null) { | ||||
|                         val tmpFile = File(file.path + ".tmp") | ||||
|                         try { | ||||
|                             // Retrieve destination stream, create parent folders if needed. | ||||
|                             val output = try { | ||||
|                                 tmpFile.outputStream() | ||||
|                             } catch (e: FileNotFoundException) { | ||||
|                                 tmpFile.parentFile.mkdirs() | ||||
|                                 tmpFile.outputStream() | ||||
|                             } | ||||
|             networkFetcher.loadData( | ||||
|                 priority, | ||||
|                 object : DataFetcher.DataCallback<InputStream> { | ||||
|                     override fun onDataReady(data: InputStream?) { | ||||
|                         if (data != null) { | ||||
|                             val tmpFile = File(file.path + ".tmp") | ||||
|                             try { | ||||
|                                 // Retrieve destination stream, create parent folders if needed. | ||||
|                                 val output = try { | ||||
|                                     tmpFile.outputStream() | ||||
|                                 } catch (e: FileNotFoundException) { | ||||
|                                     tmpFile.parentFile.mkdirs() | ||||
|                                     tmpFile.outputStream() | ||||
|                                 } | ||||
|  | ||||
|                             // Copy the file and rename to the original. | ||||
|                             data.use { output.use { data.copyTo(output) } } | ||||
|                             tmpFile.renameTo(file) | ||||
|                             loadFromFile(callback) | ||||
|                         } catch (e: Exception) { | ||||
|                             tmpFile.delete() | ||||
|                             callback.onLoadFailed(e) | ||||
|                                 // Copy the file and rename to the original. | ||||
|                                 data.use { output.use { data.copyTo(output) } } | ||||
|                                 tmpFile.renameTo(file) | ||||
|                                 loadFromFile(callback) | ||||
|                             } catch (e: Exception) { | ||||
|                                 tmpFile.delete() | ||||
|                                 callback.onLoadFailed(e) | ||||
|                             } | ||||
|                         } else { | ||||
|                             callback.onLoadFailed(Exception("Null data")) | ||||
|                         } | ||||
|                     } else { | ||||
|                         callback.onLoadFailed(Exception("Null data")) | ||||
|                     } | ||||
|  | ||||
|                     override fun onLoadFailed(e: Exception) { | ||||
|                         callback.onLoadFailed(e) | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 override fun onLoadFailed(e: Exception) { | ||||
|                     callback.onLoadFailed(e) | ||||
|                 } | ||||
|             }) | ||||
|             ) | ||||
|         } else { | ||||
|             loadFromFile(callback) | ||||
|         } | ||||
|   | ||||
| @@ -27,8 +27,10 @@ class TachiGlideModule : AppGlideModule() { | ||||
|     override fun applyOptions(context: Context, builder: GlideBuilder) { | ||||
|         builder.setDiskCache(InternalCacheDiskCacheFactory(context, 50 * 1024 * 1024)) | ||||
|         builder.setDefaultRequestOptions(RequestOptions().format(DecodeFormat.PREFER_RGB_565)) | ||||
|         builder.setDefaultTransitionOptions(Drawable::class.java, | ||||
|                 DrawableTransitionOptions.withCrossFade()) | ||||
|         builder.setDefaultTransitionOptions( | ||||
|             Drawable::class.java, | ||||
|             DrawableTransitionOptions.withCrossFade() | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     override fun registerComponents(context: Context, glide: Glide, registry: Registry) { | ||||
| @@ -36,7 +38,10 @@ class TachiGlideModule : AppGlideModule() { | ||||
|  | ||||
|         registry.replace(GlideUrl::class.java, InputStream::class.java, networkFactory) | ||||
|         registry.append(MangaThumbnail::class.java, InputStream::class.java, MangaThumbnailModelLoader.Factory()) | ||||
|         registry.append(InputStream::class.java, InputStream::class.java, PassthroughModelLoader | ||||
|                 .Factory()) | ||||
|         registry.append( | ||||
|             InputStream::class.java, InputStream::class.java, | ||||
|             PassthroughModelLoader | ||||
|                 .Factory() | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -14,7 +14,7 @@ import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) : | ||||
|         Worker(context, workerParams) { | ||||
|     Worker(context, workerParams) { | ||||
|  | ||||
|     override fun doWork(): Result { | ||||
|         LibraryUpdateService.start(context) | ||||
| @@ -30,22 +30,24 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet | ||||
|             if (interval > 0) { | ||||
|                 val restrictions = preferences.libraryUpdateRestriction()!! | ||||
|                 val acRestriction = "ac" in restrictions | ||||
|                 val wifiRestriction = if ("wifi" in restrictions) | ||||
|                 val wifiRestriction = if ("wifi" in restrictions) { | ||||
|                     NetworkType.UNMETERED | ||||
|                 else | ||||
|                 } else { | ||||
|                     NetworkType.CONNECTED | ||||
|                 } | ||||
|  | ||||
|                 val constraints = Constraints.Builder() | ||||
|                         .setRequiredNetworkType(wifiRestriction) | ||||
|                         .setRequiresCharging(acRestriction) | ||||
|                         .build() | ||||
|                     .setRequiredNetworkType(wifiRestriction) | ||||
|                     .setRequiresCharging(acRestriction) | ||||
|                     .build() | ||||
|  | ||||
|                 val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>( | ||||
|                         interval.toLong(), TimeUnit.HOURS, | ||||
|                         10, TimeUnit.MINUTES) | ||||
|                         .addTag(TAG) | ||||
|                         .setConstraints(constraints) | ||||
|                         .build() | ||||
|                     interval.toLong(), TimeUnit.HOURS, | ||||
|                     10, TimeUnit.MINUTES | ||||
|                 ) | ||||
|                     .addTag(TAG) | ||||
|                     .setConstraints(constraints) | ||||
|                     .build() | ||||
|  | ||||
|                 WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request) | ||||
|             } else { | ||||
|   | ||||
| @@ -8,8 +8,9 @@ import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| object LibraryUpdateRanker { | ||||
|  | ||||
|     val rankingScheme = listOf( | ||||
|             (this::lexicographicRanking)(), | ||||
|             (this::latestFirstRanking)()) | ||||
|         (this::lexicographicRanking)(), | ||||
|         (this::latestFirstRanking)() | ||||
|     ) | ||||
|  | ||||
|     /** | ||||
|      * Provides a total ordering over all the Mangas. | ||||
| @@ -22,7 +23,7 @@ object LibraryUpdateRanker { | ||||
|      */ | ||||
|     fun latestFirstRanking(): Comparator<Manga> { | ||||
|         return Comparator { mangaFirst: Manga, | ||||
|                             mangaSecond: Manga -> | ||||
|             mangaSecond: Manga -> | ||||
|             compareValues(mangaSecond.last_update, mangaFirst.last_update) | ||||
|         } | ||||
|     } | ||||
| @@ -35,7 +36,7 @@ object LibraryUpdateRanker { | ||||
|      */ | ||||
|     fun lexicographicRanking(): Comparator<Manga> { | ||||
|         return Comparator { mangaFirst: Manga, | ||||
|                             mangaSecond: Manga -> | ||||
|             mangaSecond: Manga -> | ||||
|             compareValues(mangaFirst.title, mangaSecond.title) | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -184,7 +184,8 @@ class LibraryUpdateService( | ||||
|         super.onCreate() | ||||
|         startForeground(Notifications.ID_LIBRARY_PROGRESS, progressNotificationBuilder.build()) | ||||
|         wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock( | ||||
|                 PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock") | ||||
|             PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock" | ||||
|         ) | ||||
|         wakeLock.acquire() | ||||
|     } | ||||
|  | ||||
| @@ -218,33 +219,37 @@ class LibraryUpdateService( | ||||
|     override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { | ||||
|         if (intent == null) return START_NOT_STICKY | ||||
|         val target = intent.getSerializableExtra(KEY_TARGET) as? Target | ||||
|                 ?: return START_NOT_STICKY | ||||
|             ?: return START_NOT_STICKY | ||||
|  | ||||
|         // Unsubscribe from any previous subscription if needed. | ||||
|         subscription?.unsubscribe() | ||||
|  | ||||
|         // Update favorite manga. Destroy service when completed or in case of an error. | ||||
|         subscription = Observable | ||||
|                 .defer { | ||||
|                     val selectedScheme = preferences.libraryUpdatePrioritization().get() | ||||
|                     val mangaList = getMangaToUpdate(intent, target) | ||||
|                             .sortedWith(rankingScheme[selectedScheme]) | ||||
|             .defer { | ||||
|                 val selectedScheme = preferences.libraryUpdatePrioritization().get() | ||||
|                 val mangaList = getMangaToUpdate(intent, target) | ||||
|                     .sortedWith(rankingScheme[selectedScheme]) | ||||
|  | ||||
|                     // Update either chapter list or manga details. | ||||
|                     when (target) { | ||||
|                         Target.CHAPTERS -> updateChapterList(mangaList) | ||||
|                         Target.DETAILS -> updateDetails(mangaList) | ||||
|                         Target.TRACKING -> updateTrackings(mangaList) | ||||
|                     } | ||||
|                 // Update either chapter list or manga details. | ||||
|                 when (target) { | ||||
|                     Target.CHAPTERS -> updateChapterList(mangaList) | ||||
|                     Target.DETAILS -> updateDetails(mangaList) | ||||
|                     Target.TRACKING -> updateTrackings(mangaList) | ||||
|                 } | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .subscribe({ | ||||
|                 }, { | ||||
|             } | ||||
|             .subscribeOn(Schedulers.io()) | ||||
|             .subscribe( | ||||
|                 { | ||||
|                 }, | ||||
|                 { | ||||
|                     Timber.e(it) | ||||
|                     stopSelf(startId) | ||||
|                 }, { | ||||
|                 }, | ||||
|                 { | ||||
|                     stopSelf(startId) | ||||
|                 }) | ||||
|                 } | ||||
|             ) | ||||
|  | ||||
|         return START_REDELIVER_INTENT | ||||
|     } | ||||
| @@ -259,16 +264,17 @@ class LibraryUpdateService( | ||||
|     fun getMangaToUpdate(intent: Intent, target: Target): List<LibraryManga> { | ||||
|         val categoryId = intent.getIntExtra(KEY_CATEGORY, -1) | ||||
|  | ||||
|         var listToUpdate = if (categoryId != -1) | ||||
|         var listToUpdate = if (categoryId != -1) { | ||||
|             db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId } | ||||
|         else { | ||||
|         } else { | ||||
|             val categoriesToUpdate = preferences.libraryUpdateCategories().get().map(String::toInt) | ||||
|             if (categoriesToUpdate.isNotEmpty()) | ||||
|             if (categoriesToUpdate.isNotEmpty()) { | ||||
|                 db.getLibraryMangas().executeAsBlocking() | ||||
|                         .filter { it.category in categoriesToUpdate } | ||||
|                         .distinctBy { it.id } | ||||
|             else | ||||
|                     .filter { it.category in categoriesToUpdate } | ||||
|                     .distinctBy { it.id } | ||||
|             } else { | ||||
|                 db.getLibraryMangas().executeAsBlocking().distinctBy { it.id } | ||||
|             } | ||||
|         } | ||||
|         if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) { | ||||
|             listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED } | ||||
| @@ -302,55 +308,57 @@ class LibraryUpdateService( | ||||
|  | ||||
|         // Emit each manga and update it sequentially. | ||||
|         return Observable.from(mangaToUpdate) | ||||
|                 // Notify manga that will update. | ||||
|                 .doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) } | ||||
|                 // Update the chapters of the manga. | ||||
|                 .concatMap { manga -> | ||||
|                     updateManga(manga) | ||||
|                             // If there's any error, return empty update and continue. | ||||
|                             .onErrorReturn { | ||||
|                                 failedUpdates.add(manga) | ||||
|                                 Pair(emptyList(), emptyList()) | ||||
|                             } | ||||
|                             // Filter out mangas without new chapters (or failed). | ||||
|                             .filter { pair -> pair.first.isNotEmpty() } | ||||
|                             .doOnNext { | ||||
|                                 if (downloadNew && (categoriesToDownload.isEmpty() || | ||||
|                                                 manga.category in categoriesToDownload)) { | ||||
|  | ||||
|                                     downloadChapters(manga, it.first) | ||||
|                                     hasDownloads = true | ||||
|                                 } | ||||
|                             } | ||||
|                             // Convert to the manga that contains new chapters. | ||||
|                             .map { | ||||
|                                 Pair( | ||||
|                                         manga, | ||||
|                                         (it.first.sortedByDescending { ch -> ch.source_order }.toTypedArray()) | ||||
|                                 ) | ||||
|                             } | ||||
|                 } | ||||
|                 // Add manga with new chapters to the list. | ||||
|                 .doOnNext { manga -> | ||||
|                     // Add to the list | ||||
|                     newUpdates.add(manga) | ||||
|                 } | ||||
|                 // Notify result of the overall update. | ||||
|                 .doOnCompleted { | ||||
|                     if (newUpdates.isNotEmpty()) { | ||||
|                         showUpdateNotifications(newUpdates) | ||||
|                         if (downloadNew && hasDownloads) { | ||||
|                             DownloadService.start(this) | ||||
|             // Notify manga that will update. | ||||
|             .doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) } | ||||
|             // Update the chapters of the manga. | ||||
|             .concatMap { manga -> | ||||
|                 updateManga(manga) | ||||
|                     // If there's any error, return empty update and continue. | ||||
|                     .onErrorReturn { | ||||
|                         failedUpdates.add(manga) | ||||
|                         Pair(emptyList(), emptyList()) | ||||
|                     } | ||||
|                     // Filter out mangas without new chapters (or failed). | ||||
|                     .filter { pair -> pair.first.isNotEmpty() } | ||||
|                     .doOnNext { | ||||
|                         if (downloadNew && ( | ||||
|                             categoriesToDownload.isEmpty() || | ||||
|                                 manga.category in categoriesToDownload | ||||
|                             ) | ||||
|                         ) { | ||||
|                             downloadChapters(manga, it.first) | ||||
|                             hasDownloads = true | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     if (failedUpdates.isNotEmpty()) { | ||||
|                         Timber.e("Failed updating: ${failedUpdates.map { it.title }}") | ||||
|                     // Convert to the manga that contains new chapters. | ||||
|                     .map { | ||||
|                         Pair( | ||||
|                             manga, | ||||
|                             (it.first.sortedByDescending { ch -> ch.source_order }.toTypedArray()) | ||||
|                         ) | ||||
|                     } | ||||
|             } | ||||
|             // Add manga with new chapters to the list. | ||||
|             .doOnNext { manga -> | ||||
|                 // Add to the list | ||||
|                 newUpdates.add(manga) | ||||
|             } | ||||
|             // Notify result of the overall update. | ||||
|             .doOnCompleted { | ||||
|                 if (newUpdates.isNotEmpty()) { | ||||
|                     showUpdateNotifications(newUpdates) | ||||
|                     if (downloadNew && hasDownloads) { | ||||
|                         DownloadService.start(this) | ||||
|                     } | ||||
|  | ||||
|                     cancelProgressNotification() | ||||
|                 } | ||||
|                 .map { manga -> manga.first } | ||||
|  | ||||
|                 if (failedUpdates.isNotEmpty()) { | ||||
|                     Timber.e("Failed updating: ${failedUpdates.map { it.title }}") | ||||
|                 } | ||||
|  | ||||
|                 cancelProgressNotification() | ||||
|             } | ||||
|             .map { manga -> manga.first } | ||||
|     } | ||||
|  | ||||
|     fun downloadChapters(manga: Manga, chapters: List<Chapter>) { | ||||
| @@ -373,7 +381,7 @@ class LibraryUpdateService( | ||||
|     fun updateManga(manga: Manga): Observable<Pair<List<Chapter>, List<Chapter>>> { | ||||
|         val source = sourceManager.get(manga.source) as? HttpSource ?: return Observable.empty() | ||||
|         return source.fetchChapterList(manga) | ||||
|                 .map { syncChaptersWithSource(db, it, manga, source) } | ||||
|             .map { syncChaptersWithSource(db, it, manga, source) } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -389,24 +397,24 @@ class LibraryUpdateService( | ||||
|  | ||||
|         // Emit each manga and update it sequentially. | ||||
|         return Observable.from(mangaToUpdate) | ||||
|                 // Notify manga that will update. | ||||
|                 .doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) } | ||||
|                 // Update the details of the manga. | ||||
|                 .concatMap { manga -> | ||||
|                     val source = sourceManager.get(manga.source) as? HttpSource | ||||
|                             ?: return@concatMap Observable.empty<LibraryManga>() | ||||
|             // Notify manga that will update. | ||||
|             .doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) } | ||||
|             // Update the details of the manga. | ||||
|             .concatMap { manga -> | ||||
|                 val source = sourceManager.get(manga.source) as? HttpSource | ||||
|                     ?: return@concatMap Observable.empty<LibraryManga>() | ||||
|  | ||||
|                     source.fetchMangaDetails(manga) | ||||
|                             .map { networkManga -> | ||||
|                                 manga.copyFrom(networkManga) | ||||
|                                 db.insertManga(manga).executeAsBlocking() | ||||
|                                 manga | ||||
|                             } | ||||
|                             .onErrorReturn { manga } | ||||
|                 } | ||||
|                 .doOnCompleted { | ||||
|                     cancelProgressNotification() | ||||
|                 } | ||||
|                 source.fetchMangaDetails(manga) | ||||
|                     .map { networkManga -> | ||||
|                         manga.copyFrom(networkManga) | ||||
|                         db.insertManga(manga).executeAsBlocking() | ||||
|                         manga | ||||
|                     } | ||||
|                     .onErrorReturn { manga } | ||||
|             } | ||||
|             .doOnCompleted { | ||||
|                 cancelProgressNotification() | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -421,28 +429,28 @@ class LibraryUpdateService( | ||||
|  | ||||
|         // Emit each manga and update it sequentially. | ||||
|         return Observable.from(mangaToUpdate) | ||||
|                 // Notify manga that will update. | ||||
|                 .doOnNext { showProgressNotification(it, count++, mangaToUpdate.size) } | ||||
|                 // Update the tracking details. | ||||
|                 .concatMap { manga -> | ||||
|                     val tracks = db.getTracks(manga).executeAsBlocking() | ||||
|             // Notify manga that will update. | ||||
|             .doOnNext { showProgressNotification(it, count++, mangaToUpdate.size) } | ||||
|             // Update the tracking details. | ||||
|             .concatMap { manga -> | ||||
|                 val tracks = db.getTracks(manga).executeAsBlocking() | ||||
|  | ||||
|                     Observable.from(tracks) | ||||
|                             .concatMap { track -> | ||||
|                                 val service = trackManager.getService(track.sync_id) | ||||
|                                 if (service != null && service in loggedServices) { | ||||
|                                     service.refresh(track) | ||||
|                                             .doOnNext { db.insertTrack(it).executeAsBlocking() } | ||||
|                                             .onErrorReturn { track } | ||||
|                                 } else { | ||||
|                                     Observable.empty() | ||||
|                                 } | ||||
|                             } | ||||
|                             .map { manga } | ||||
|                 } | ||||
|                 .doOnCompleted { | ||||
|                     cancelProgressNotification() | ||||
|                 } | ||||
|                 Observable.from(tracks) | ||||
|                     .concatMap { track -> | ||||
|                         val service = trackManager.getService(track.sync_id) | ||||
|                         if (service != null && service in loggedServices) { | ||||
|                             service.refresh(track) | ||||
|                                 .doOnNext { db.insertTrack(it).executeAsBlocking() } | ||||
|                                 .onErrorReturn { track } | ||||
|                         } else { | ||||
|                             Observable.empty() | ||||
|                         } | ||||
|                     } | ||||
|                     .map { manga } | ||||
|             } | ||||
|             .doOnCompleted { | ||||
|                 cancelProgressNotification() | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -453,15 +461,19 @@ class LibraryUpdateService( | ||||
|      * @param total the total progress. | ||||
|      */ | ||||
|     private fun showProgressNotification(manga: Manga, current: Int, total: Int) { | ||||
|         val title = if (preferences.hideNotificationContent()) | ||||
|         val title = if (preferences.hideNotificationContent()) { | ||||
|             getString(R.string.notification_check_updates) | ||||
|         else | ||||
|         } else { | ||||
|             manga.title | ||||
|         } | ||||
|  | ||||
|         notificationManager.notify(Notifications.ID_LIBRARY_PROGRESS, progressNotificationBuilder | ||||
|         notificationManager.notify( | ||||
|             Notifications.ID_LIBRARY_PROGRESS, | ||||
|             progressNotificationBuilder | ||||
|                 .setContentTitle(title) | ||||
|                 .setProgress(total, current, false) | ||||
|                 .build()) | ||||
|                 .build() | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -476,31 +488,38 @@ class LibraryUpdateService( | ||||
|  | ||||
|         NotificationManagerCompat.from(this).apply { | ||||
|             // Parent group notification | ||||
|             notify(Notifications.ID_NEW_CHAPTERS, notification(Notifications.CHANNEL_NEW_CHAPTERS) { | ||||
|                 setContentTitle(getString(R.string.notification_new_chapters)) | ||||
|                 if (updates.size == 1 && !preferences.hideNotificationContent()) { | ||||
|                     setContentText(updates.first().first.title.chop(NOTIF_TITLE_MAX_LEN)) | ||||
|                 } else { | ||||
|                     setContentText(resources.getQuantityString(R.plurals.notification_new_chapters_summary, updates.size, updates.size)) | ||||
|             notify( | ||||
|                 Notifications.ID_NEW_CHAPTERS, | ||||
|                 notification(Notifications.CHANNEL_NEW_CHAPTERS) { | ||||
|                     setContentTitle(getString(R.string.notification_new_chapters)) | ||||
|                     if (updates.size == 1 && !preferences.hideNotificationContent()) { | ||||
|                         setContentText(updates.first().first.title.chop(NOTIF_TITLE_MAX_LEN)) | ||||
|                     } else { | ||||
|                         setContentText(resources.getQuantityString(R.plurals.notification_new_chapters_summary, updates.size, updates.size)) | ||||
|  | ||||
|                     if (!preferences.hideNotificationContent()) { | ||||
|                         setStyle(NotificationCompat.BigTextStyle().bigText(updates.joinToString("\n") { | ||||
|                             it.first.title.chop(NOTIF_TITLE_MAX_LEN) | ||||
|                         })) | ||||
|                         if (!preferences.hideNotificationContent()) { | ||||
|                             setStyle( | ||||
|                                 NotificationCompat.BigTextStyle().bigText( | ||||
|                                     updates.joinToString("\n") { | ||||
|                                         it.first.title.chop(NOTIF_TITLE_MAX_LEN) | ||||
|                                     } | ||||
|                                 ) | ||||
|                             ) | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     setSmallIcon(R.drawable.ic_tachi) | ||||
|                     setLargeIcon(notificationBitmap) | ||||
|  | ||||
|                     setGroup(Notifications.GROUP_NEW_CHAPTERS) | ||||
|                     setGroupAlertBehavior(GROUP_ALERT_SUMMARY) | ||||
|                     setGroupSummary(true) | ||||
|                     priority = NotificationCompat.PRIORITY_HIGH | ||||
|  | ||||
|                     setContentIntent(getNotificationIntent()) | ||||
|                     setAutoCancel(true) | ||||
|                 } | ||||
|  | ||||
|                 setSmallIcon(R.drawable.ic_tachi) | ||||
|                 setLargeIcon(notificationBitmap) | ||||
|  | ||||
|                 setGroup(Notifications.GROUP_NEW_CHAPTERS) | ||||
|                 setGroupAlertBehavior(GROUP_ALERT_SUMMARY) | ||||
|                 setGroupSummary(true) | ||||
|                 priority = NotificationCompat.PRIORITY_HIGH | ||||
|  | ||||
|                 setContentIntent(getNotificationIntent()) | ||||
|                 setAutoCancel(true) | ||||
|             }) | ||||
|             ) | ||||
|  | ||||
|             // Per-manga notification | ||||
|             if (!preferences.hideNotificationContent()) { | ||||
| @@ -536,13 +555,21 @@ class LibraryUpdateService( | ||||
|             setAutoCancel(true) | ||||
|  | ||||
|             // Mark chapters as read action | ||||
|             addAction(R.drawable.ic_glasses_black_24dp, getString(R.string.action_mark_as_read), | ||||
|                     NotificationReceiver.markAsReadPendingBroadcast(this@LibraryUpdateService, | ||||
|                             manga, chapters, Notifications.ID_NEW_CHAPTERS)) | ||||
|             addAction( | ||||
|                 R.drawable.ic_glasses_black_24dp, getString(R.string.action_mark_as_read), | ||||
|                 NotificationReceiver.markAsReadPendingBroadcast( | ||||
|                     this@LibraryUpdateService, | ||||
|                     manga, chapters, Notifications.ID_NEW_CHAPTERS | ||||
|                 ) | ||||
|             ) | ||||
|             // View chapters action | ||||
|             addAction(R.drawable.ic_book_24dp, getString(R.string.action_view_chapters), | ||||
|                     NotificationReceiver.openChapterPendingActivity(this@LibraryUpdateService, | ||||
|                             manga, Notifications.ID_NEW_CHAPTERS)) | ||||
|             addAction( | ||||
|                 R.drawable.ic_book_24dp, getString(R.string.action_view_chapters), | ||||
|                 NotificationReceiver.openChapterPendingActivity( | ||||
|                     this@LibraryUpdateService, | ||||
|                     manga, Notifications.ID_NEW_CHAPTERS | ||||
|                 ) | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -556,28 +583,31 @@ class LibraryUpdateService( | ||||
|     private fun getMangaIcon(manga: Manga): Bitmap? { | ||||
|         return try { | ||||
|             Glide.with(this) | ||||
|                     .asBitmap() | ||||
|                     .load(manga.toMangaThumbnail()) | ||||
|                     .dontTransform() | ||||
|                     .centerCrop() | ||||
|                     .circleCrop() | ||||
|                     .override(NOTIF_ICON_SIZE, NOTIF_ICON_SIZE) | ||||
|                     .submit() | ||||
|                     .get() | ||||
|                 .asBitmap() | ||||
|                 .load(manga.toMangaThumbnail()) | ||||
|                 .dontTransform() | ||||
|                 .centerCrop() | ||||
|                 .circleCrop() | ||||
|                 .override(NOTIF_ICON_SIZE, NOTIF_ICON_SIZE) | ||||
|                 .submit() | ||||
|                 .get() | ||||
|         } catch (e: Exception) { | ||||
|             null | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun getNewChaptersDescription(chapters: Array<Chapter>): String { | ||||
|         val formatter = DecimalFormat("#.###", DecimalFormatSymbols() | ||||
|                 .apply { decimalSeparator = '.' }) | ||||
|         val formatter = DecimalFormat( | ||||
|             "#.###", | ||||
|             DecimalFormatSymbols() | ||||
|                 .apply { decimalSeparator = '.' } | ||||
|         ) | ||||
|  | ||||
|         val displayableChapterNumbers = chapters | ||||
|                 .filter { it.isRecognizedNumber } | ||||
|                 .sortedBy { it.chapter_number } | ||||
|                 .map { formatter.format(it.chapter_number) } | ||||
|                 .toSet() | ||||
|             .filter { it.isRecognizedNumber } | ||||
|             .sortedBy { it.chapter_number } | ||||
|             .map { formatter.format(it.chapter_number) } | ||||
|             .toSet() | ||||
|  | ||||
|         return when (displayableChapterNumbers.size) { | ||||
|             // No sensible chapter numbers to show (i.e. no chapters have parsed chapter number) | ||||
|   | ||||
| @@ -54,22 +54,35 @@ class NotificationReceiver : BroadcastReceiver() { | ||||
|             // Clear the download queue | ||||
|             ACTION_CLEAR_DOWNLOADS -> downloadManager.clearQueue(true) | ||||
|             // Launch share activity and dismiss notification | ||||
|             ACTION_SHARE_IMAGE -> shareImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION), | ||||
|                     intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)) | ||||
|             ACTION_SHARE_IMAGE -> | ||||
|                 shareImage( | ||||
|                     context, intent.getStringExtra(EXTRA_FILE_LOCATION), | ||||
|                     intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1) | ||||
|                 ) | ||||
|             // Delete image from path and dismiss notification | ||||
|             ACTION_DELETE_IMAGE -> deleteImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION), | ||||
|                     intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)) | ||||
|             ACTION_DELETE_IMAGE -> | ||||
|                 deleteImage( | ||||
|                     context, intent.getStringExtra(EXTRA_FILE_LOCATION), | ||||
|                     intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1) | ||||
|                 ) | ||||
|             // Share backup file | ||||
|             ACTION_SHARE_BACKUP -> shareBackup(context, intent.getParcelableExtra(EXTRA_URI), | ||||
|                     intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)) | ||||
|             ACTION_CANCEL_RESTORE -> cancelRestore(context, | ||||
|                     intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)) | ||||
|             ACTION_SHARE_BACKUP -> | ||||
|                 shareBackup( | ||||
|                     context, intent.getParcelableExtra(EXTRA_URI), | ||||
|                     intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1) | ||||
|                 ) | ||||
|             ACTION_CANCEL_RESTORE -> cancelRestore( | ||||
|                 context, | ||||
|                 intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1) | ||||
|             ) | ||||
|             // Cancel library update and dismiss notification | ||||
|             ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context, Notifications.ID_LIBRARY_PROGRESS) | ||||
|             // Open reader activity | ||||
|             ACTION_OPEN_CHAPTER -> { | ||||
|                 openChapter(context, intent.getLongExtra(EXTRA_MANGA_ID, -1), | ||||
|                         intent.getLongExtra(EXTRA_CHAPTER_ID, -1)) | ||||
|                 openChapter( | ||||
|                     context, intent.getLongExtra(EXTRA_MANGA_ID, -1), | ||||
|                     intent.getLongExtra(EXTRA_CHAPTER_ID, -1) | ||||
|                 ) | ||||
|             } | ||||
|             // Mark updated manga chapters as read | ||||
|             ACTION_MARK_AS_READ -> { | ||||
| @@ -208,19 +221,19 @@ class NotificationReceiver : BroadcastReceiver() { | ||||
|  | ||||
|         launchIO { | ||||
|             chapterUrls.mapNotNull { db.getChapter(it, mangaId).executeAsBlocking() } | ||||
|                     .forEach { | ||||
|                         it.read = true | ||||
|                         db.updateChapterProgress(it).executeAsBlocking() | ||||
|                         if (preferences.removeAfterMarkedAsRead()) { | ||||
|                             val manga = db.getManga(mangaId).executeAsBlocking() | ||||
|                             if (manga != null) { | ||||
|                                 val source = sourceManager.get(manga.source) | ||||
|                                 if (source != null) { | ||||
|                                     downloadManager.deleteChapters(listOf(it), manga, source) | ||||
|                                 } | ||||
|                 .forEach { | ||||
|                     it.read = true | ||||
|                     db.updateChapterProgress(it).executeAsBlocking() | ||||
|                     if (preferences.removeAfterMarkedAsRead()) { | ||||
|                         val manga = db.getManga(mangaId).executeAsBlocking() | ||||
|                         if (manga != null) { | ||||
|                             val source = sourceManager.get(manga.source) | ||||
|                             if (source != null) { | ||||
|                                 downloadManager.deleteChapters(listOf(it), manga, source) | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -427,11 +440,11 @@ class NotificationReceiver : BroadcastReceiver() { | ||||
|          */ | ||||
|         internal fun openChapterPendingActivity(context: Context, manga: Manga, groupId: Int): PendingIntent { | ||||
|             val newIntent = | ||||
|                     Intent(context, MainActivity::class.java).setAction(MainActivity.SHORTCUT_MANGA) | ||||
|                             .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) | ||||
|                             .putExtra(MangaController.MANGA_EXTRA, manga.id) | ||||
|                             .putExtra("notificationId", manga.id.hashCode()) | ||||
|                             .putExtra("groupId", groupId) | ||||
|                 Intent(context, MainActivity::class.java).setAction(MainActivity.SHORTCUT_MANGA) | ||||
|                     .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) | ||||
|                     .putExtra(MangaController.MANGA_EXTRA, manga.id) | ||||
|                     .putExtra("notificationId", manga.id.hashCode()) | ||||
|                     .putExtra("groupId", groupId) | ||||
|             return PendingIntent.getActivity(context, manga.id.hashCode(), newIntent, PendingIntent.FLAG_UPDATE_CURRENT) | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -61,24 +61,36 @@ object Notifications { | ||||
|         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return | ||||
|  | ||||
|         val channels = listOf( | ||||
|                 NotificationChannel(CHANNEL_COMMON, context.getString(R.string.channel_common), | ||||
|                         NotificationManager.IMPORTANCE_LOW), | ||||
|                 NotificationChannel(CHANNEL_LIBRARY, context.getString(R.string.channel_library), | ||||
|                         NotificationManager.IMPORTANCE_LOW).apply { | ||||
|                     setShowBadge(false) | ||||
|                 }, | ||||
|                 NotificationChannel(CHANNEL_DOWNLOADER, context.getString(R.string.channel_downloader), | ||||
|                         NotificationManager.IMPORTANCE_LOW).apply { | ||||
|                     setShowBadge(false) | ||||
|                 }, | ||||
|                 NotificationChannel(CHANNEL_NEW_CHAPTERS, context.getString(R.string.channel_new_chapters), | ||||
|                         NotificationManager.IMPORTANCE_DEFAULT), | ||||
|                 NotificationChannel(CHANNEL_UPDATES_TO_EXTS, context.getString(R.string.channel_ext_updates), | ||||
|                         NotificationManager.IMPORTANCE_DEFAULT), | ||||
|                 NotificationChannel(CHANNEL_BACKUP_RESTORE, context.getString(R.string.channel_backup_restore), | ||||
|                     NotificationManager.IMPORTANCE_HIGH).apply { | ||||
|                     setShowBadge(false) | ||||
|                 } | ||||
|             NotificationChannel( | ||||
|                 CHANNEL_COMMON, context.getString(R.string.channel_common), | ||||
|                 NotificationManager.IMPORTANCE_LOW | ||||
|             ), | ||||
|             NotificationChannel( | ||||
|                 CHANNEL_LIBRARY, context.getString(R.string.channel_library), | ||||
|                 NotificationManager.IMPORTANCE_LOW | ||||
|             ).apply { | ||||
|                 setShowBadge(false) | ||||
|             }, | ||||
|             NotificationChannel( | ||||
|                 CHANNEL_DOWNLOADER, context.getString(R.string.channel_downloader), | ||||
|                 NotificationManager.IMPORTANCE_LOW | ||||
|             ).apply { | ||||
|                 setShowBadge(false) | ||||
|             }, | ||||
|             NotificationChannel( | ||||
|                 CHANNEL_NEW_CHAPTERS, context.getString(R.string.channel_new_chapters), | ||||
|                 NotificationManager.IMPORTANCE_DEFAULT | ||||
|             ), | ||||
|             NotificationChannel( | ||||
|                 CHANNEL_UPDATES_TO_EXTS, context.getString(R.string.channel_ext_updates), | ||||
|                 NotificationManager.IMPORTANCE_DEFAULT | ||||
|             ), | ||||
|             NotificationChannel( | ||||
|                 CHANNEL_BACKUP_RESTORE, context.getString(R.string.channel_backup_restore), | ||||
|                 NotificationManager.IMPORTANCE_HIGH | ||||
|             ).apply { | ||||
|                 setShowBadge(false) | ||||
|             } | ||||
|         ) | ||||
|         context.notificationManager.createNotificationChannels(channels) | ||||
|     } | ||||
|   | ||||
| @@ -45,12 +45,20 @@ class PreferencesHelper(val context: Context) { | ||||
|     private val flowPrefs = FlowSharedPreferences(prefs) | ||||
|  | ||||
|     private val defaultDownloadsDir = Uri.fromFile( | ||||
|             File(Environment.getExternalStorageDirectory().absolutePath + File.separator + | ||||
|                     context.getString(R.string.app_name), "downloads")) | ||||
|         File( | ||||
|             Environment.getExternalStorageDirectory().absolutePath + File.separator + | ||||
|                 context.getString(R.string.app_name), | ||||
|             "downloads" | ||||
|         ) | ||||
|     ) | ||||
|  | ||||
|     private val defaultBackupDir = Uri.fromFile( | ||||
|             File(Environment.getExternalStorageDirectory().absolutePath + File.separator + | ||||
|                     context.getString(R.string.app_name), "backup")) | ||||
|         File( | ||||
|             Environment.getExternalStorageDirectory().absolutePath + File.separator + | ||||
|                 context.getString(R.string.app_name), | ||||
|             "backup" | ||||
|         ) | ||||
|     ) | ||||
|  | ||||
|     fun startScreen() = prefs.getInt(Keys.startScreen, 1) | ||||
|  | ||||
| @@ -148,9 +156,9 @@ class PreferencesHelper(val context: Context) { | ||||
|  | ||||
|     fun setTrackCredentials(sync: TrackService, username: String, password: String) { | ||||
|         prefs.edit() | ||||
|                 .putString(Keys.trackUsername(sync.id), username) | ||||
|                 .putString(Keys.trackPassword(sync.id), password) | ||||
|                 .apply() | ||||
|             .putString(Keys.trackUsername(sync.id), username) | ||||
|             .putString(Keys.trackPassword(sync.id), password) | ||||
|             .apply() | ||||
|     } | ||||
|  | ||||
|     fun trackToken(sync: TrackService) = flowPrefs.getString(Keys.trackToken(sync.id), "") | ||||
|   | ||||
| @@ -63,7 +63,7 @@ abstract class TrackService(val id: Int) { | ||||
|  | ||||
|     open val isLogged: Boolean | ||||
|         get() = getUsername().isNotEmpty() && | ||||
|                 getPassword().isNotEmpty() | ||||
|             getPassword().isNotEmpty() | ||||
|  | ||||
|     fun getUsername() = preferences.trackUsername(this)!! | ||||
|  | ||||
|   | ||||
| @@ -150,18 +150,18 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) { | ||||
|  | ||||
|     override fun bind(track: Track): Observable<Track> { | ||||
|         return api.findLibManga(track, getUsername().toInt()) | ||||
|                 .flatMap { remoteTrack -> | ||||
|                     if (remoteTrack != null) { | ||||
|                         track.copyPersonalFrom(remoteTrack) | ||||
|                         track.library_id = remoteTrack.library_id | ||||
|                         update(track) | ||||
|                     } else { | ||||
|                         // Set default fields if it's not found in the list | ||||
|                         track.score = DEFAULT_SCORE.toFloat() | ||||
|                         track.status = DEFAULT_STATUS | ||||
|                         add(track) | ||||
|                     } | ||||
|             .flatMap { remoteTrack -> | ||||
|                 if (remoteTrack != null) { | ||||
|                     track.copyPersonalFrom(remoteTrack) | ||||
|                     track.library_id = remoteTrack.library_id | ||||
|                     update(track) | ||||
|                 } else { | ||||
|                     // Set default fields if it's not found in the list | ||||
|                     track.score = DEFAULT_SCORE.toFloat() | ||||
|                     track.status = DEFAULT_STATUS | ||||
|                     add(track) | ||||
|                 } | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     override fun search(query: String): Observable<List<TrackSearch>> { | ||||
| @@ -170,11 +170,11 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) { | ||||
|  | ||||
|     override fun refresh(track: Track): Observable<Track> { | ||||
|         return api.getLibManga(track, getUsername().toInt()) | ||||
|                 .map { remoteTrack -> | ||||
|                     track.copyPersonalFrom(remoteTrack) | ||||
|                     track.total_chapters = remoteTrack.total_chapters | ||||
|                     track | ||||
|                 } | ||||
|             .map { remoteTrack -> | ||||
|                 track.copyPersonalFrom(remoteTrack) | ||||
|                 track.total_chapters = remoteTrack.total_chapters | ||||
|                 track | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     override fun login(username: String, password: String) = login(password) | ||||
|   | ||||
| @@ -25,7 +25,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { | ||||
|     private val authClient = client.newBuilder().addInterceptor(interceptor).build() | ||||
|  | ||||
|     fun addLibManga(track: Track): Observable<Track> { | ||||
|         val query = """ | ||||
|         val query = | ||||
|             """ | ||||
|             |mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) { | ||||
|                 |SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) {  | ||||
|                 |   id  | ||||
| @@ -34,35 +35,36 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { | ||||
|             |} | ||||
|             |""".trimMargin() | ||||
|         val variables = jsonObject( | ||||
|                 "mangaId" to track.media_id, | ||||
|                 "progress" to track.last_chapter_read, | ||||
|                 "status" to track.toAnilistStatus() | ||||
|             "mangaId" to track.media_id, | ||||
|             "progress" to track.last_chapter_read, | ||||
|             "status" to track.toAnilistStatus() | ||||
|         ) | ||||
|         val payload = jsonObject( | ||||
|                 "query" to query, | ||||
|                 "variables" to variables | ||||
|             "query" to query, | ||||
|             "variables" to variables | ||||
|         ) | ||||
|         val body = payload.toString().toRequestBody(jsonMime) | ||||
|         val request = Request.Builder() | ||||
|                 .url(apiUrl) | ||||
|                 .post(body) | ||||
|                 .build() | ||||
|             .url(apiUrl) | ||||
|             .post(body) | ||||
|             .build() | ||||
|         return authClient.newCall(request) | ||||
|                 .asObservableSuccess() | ||||
|                 .map { netResponse -> | ||||
|                     val responseBody = netResponse.body?.string().orEmpty() | ||||
|                     netResponse.close() | ||||
|                     if (responseBody.isEmpty()) { | ||||
|                         throw Exception("Null Response") | ||||
|                     } | ||||
|                     val response = JsonParser.parseString(responseBody).obj | ||||
|                     track.library_id = response["data"]["SaveMediaListEntry"]["id"].asLong | ||||
|                     track | ||||
|             .asObservableSuccess() | ||||
|             .map { netResponse -> | ||||
|                 val responseBody = netResponse.body?.string().orEmpty() | ||||
|                 netResponse.close() | ||||
|                 if (responseBody.isEmpty()) { | ||||
|                     throw Exception("Null Response") | ||||
|                 } | ||||
|                 val response = JsonParser.parseString(responseBody).obj | ||||
|                 track.library_id = response["data"]["SaveMediaListEntry"]["id"].asLong | ||||
|                 track | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     fun updateLibManga(track: Track): Observable<Track> { | ||||
|         val query = """ | ||||
|         val query = | ||||
|             """ | ||||
|             |mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) { | ||||
|                 |SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) { | ||||
|                     |id | ||||
| @@ -72,29 +74,30 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { | ||||
|             |} | ||||
|             |""".trimMargin() | ||||
|         val variables = jsonObject( | ||||
|                 "listId" to track.library_id, | ||||
|                 "progress" to track.last_chapter_read, | ||||
|                 "status" to track.toAnilistStatus(), | ||||
|                 "score" to track.score.toInt() | ||||
|             "listId" to track.library_id, | ||||
|             "progress" to track.last_chapter_read, | ||||
|             "status" to track.toAnilistStatus(), | ||||
|             "score" to track.score.toInt() | ||||
|         ) | ||||
|         val payload = jsonObject( | ||||
|                 "query" to query, | ||||
|                 "variables" to variables | ||||
|             "query" to query, | ||||
|             "variables" to variables | ||||
|         ) | ||||
|         val body = payload.toString().toRequestBody(jsonMime) | ||||
|         val request = Request.Builder() | ||||
|                 .url(apiUrl) | ||||
|                 .post(body) | ||||
|                 .build() | ||||
|             .url(apiUrl) | ||||
|             .post(body) | ||||
|             .build() | ||||
|         return authClient.newCall(request) | ||||
|                 .asObservableSuccess() | ||||
|                 .map { | ||||
|                     track | ||||
|                 } | ||||
|             .asObservableSuccess() | ||||
|             .map { | ||||
|                 track | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     fun search(search: String): Observable<List<TrackSearch>> { | ||||
|         val query = """ | ||||
|         val query = | ||||
|             """ | ||||
|             |query Search(${'$'}query: String) { | ||||
|                 |Page (perPage: 50) { | ||||
|                     |media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) { | ||||
| @@ -119,35 +122,36 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { | ||||
|             |} | ||||
|             |""".trimMargin() | ||||
|         val variables = jsonObject( | ||||
|                 "query" to search | ||||
|             "query" to search | ||||
|         ) | ||||
|         val payload = jsonObject( | ||||
|                 "query" to query, | ||||
|                 "variables" to variables | ||||
|             "query" to query, | ||||
|             "variables" to variables | ||||
|         ) | ||||
|         val body = payload.toString().toRequestBody(jsonMime) | ||||
|         val request = Request.Builder() | ||||
|                 .url(apiUrl) | ||||
|                 .post(body) | ||||
|                 .build() | ||||
|             .url(apiUrl) | ||||
|             .post(body) | ||||
|             .build() | ||||
|         return authClient.newCall(request) | ||||
|                 .asObservableSuccess() | ||||
|                 .map { netResponse -> | ||||
|                     val responseBody = netResponse.body?.string().orEmpty() | ||||
|                     if (responseBody.isEmpty()) { | ||||
|                         throw Exception("Null Response") | ||||
|                     } | ||||
|                     val response = JsonParser.parseString(responseBody).obj | ||||
|                     val data = response["data"]!!.obj | ||||
|                     val page = data["Page"].obj | ||||
|                     val media = page["media"].array | ||||
|                     val entries = media.map { jsonToALManga(it.obj) } | ||||
|                     entries.map { it.toTrack() } | ||||
|             .asObservableSuccess() | ||||
|             .map { netResponse -> | ||||
|                 val responseBody = netResponse.body?.string().orEmpty() | ||||
|                 if (responseBody.isEmpty()) { | ||||
|                     throw Exception("Null Response") | ||||
|                 } | ||||
|                 val response = JsonParser.parseString(responseBody).obj | ||||
|                 val data = response["data"]!!.obj | ||||
|                 val page = data["Page"].obj | ||||
|                 val media = page["media"].array | ||||
|                 val entries = media.map { jsonToALManga(it.obj) } | ||||
|                 entries.map { it.toTrack() } | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     fun findLibManga(track: Track, userid: Int): Observable<Track?> { | ||||
|         val query = """ | ||||
|         val query = | ||||
|             """ | ||||
|             |query (${'$'}id: Int!, ${'$'}manga_id: Int!) { | ||||
|                 |Page { | ||||
|                     |mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) { | ||||
| @@ -178,37 +182,37 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { | ||||
|             |} | ||||
|             |""".trimMargin() | ||||
|         val variables = jsonObject( | ||||
|                 "id" to userid, | ||||
|                 "manga_id" to track.media_id | ||||
|             "id" to userid, | ||||
|             "manga_id" to track.media_id | ||||
|         ) | ||||
|         val payload = jsonObject( | ||||
|                 "query" to query, | ||||
|                 "variables" to variables | ||||
|             "query" to query, | ||||
|             "variables" to variables | ||||
|         ) | ||||
|         val body = payload.toString().toRequestBody(jsonMime) | ||||
|         val request = Request.Builder() | ||||
|                 .url(apiUrl) | ||||
|                 .post(body) | ||||
|                 .build() | ||||
|             .url(apiUrl) | ||||
|             .post(body) | ||||
|             .build() | ||||
|         return authClient.newCall(request) | ||||
|                 .asObservableSuccess() | ||||
|                 .map { netResponse -> | ||||
|                     val responseBody = netResponse.body?.string().orEmpty() | ||||
|                     if (responseBody.isEmpty()) { | ||||
|                         throw Exception("Null Response") | ||||
|                     } | ||||
|                     val response = JsonParser.parseString(responseBody).obj | ||||
|                     val data = response["data"]!!.obj | ||||
|                     val page = data["Page"].obj | ||||
|                     val media = page["mediaList"].array | ||||
|                     val entries = media.map { jsonToALUserManga(it.obj) } | ||||
|                     entries.firstOrNull()?.toTrack() | ||||
|             .asObservableSuccess() | ||||
|             .map { netResponse -> | ||||
|                 val responseBody = netResponse.body?.string().orEmpty() | ||||
|                 if (responseBody.isEmpty()) { | ||||
|                     throw Exception("Null Response") | ||||
|                 } | ||||
|                 val response = JsonParser.parseString(responseBody).obj | ||||
|                 val data = response["data"]!!.obj | ||||
|                 val page = data["Page"].obj | ||||
|                 val media = page["mediaList"].array | ||||
|                 val entries = media.map { jsonToALUserManga(it.obj) } | ||||
|                 entries.firstOrNull()?.toTrack() | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     fun getLibManga(track: Track, userid: Int): Observable<Track> { | ||||
|         return findLibManga(track, userid) | ||||
|                 .map { it ?: throw Exception("Could not find manga") } | ||||
|             .map { it ?: throw Exception("Could not find manga") } | ||||
|     } | ||||
|  | ||||
|     fun createOAuth(token: String): OAuth { | ||||
| @@ -216,7 +220,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { | ||||
|     } | ||||
|  | ||||
|     fun getCurrentUser(): Observable<Pair<Int, String>> { | ||||
|         val query = """ | ||||
|         val query = | ||||
|             """ | ||||
|             |query User { | ||||
|                 |Viewer { | ||||
|                     |id | ||||
| @@ -227,41 +232,48 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { | ||||
|             |} | ||||
|             |""".trimMargin() | ||||
|         val payload = jsonObject( | ||||
|                 "query" to query | ||||
|             "query" to query | ||||
|         ) | ||||
|         val body = payload.toString().toRequestBody(jsonMime) | ||||
|         val request = Request.Builder() | ||||
|                 .url(apiUrl) | ||||
|                 .post(body) | ||||
|                 .build() | ||||
|             .url(apiUrl) | ||||
|             .post(body) | ||||
|             .build() | ||||
|         return authClient.newCall(request) | ||||
|                 .asObservableSuccess() | ||||
|                 .map { netResponse -> | ||||
|                     val responseBody = netResponse.body?.string().orEmpty() | ||||
|                     if (responseBody.isEmpty()) { | ||||
|                         throw Exception("Null Response") | ||||
|                     } | ||||
|                     val response = JsonParser.parseString(responseBody).obj | ||||
|                     val data = response["data"]!!.obj | ||||
|                     val viewer = data["Viewer"].obj | ||||
|                     Pair(viewer["id"].asInt, viewer["mediaListOptions"]["scoreFormat"].asString) | ||||
|             .asObservableSuccess() | ||||
|             .map { netResponse -> | ||||
|                 val responseBody = netResponse.body?.string().orEmpty() | ||||
|                 if (responseBody.isEmpty()) { | ||||
|                     throw Exception("Null Response") | ||||
|                 } | ||||
|                 val response = JsonParser.parseString(responseBody).obj | ||||
|                 val data = response["data"]!!.obj | ||||
|                 val viewer = data["Viewer"].obj | ||||
|                 Pair(viewer["id"].asInt, viewer["mediaListOptions"]["scoreFormat"].asString) | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     private fun jsonToALManga(struct: JsonObject): ALManga { | ||||
|         val date = try { | ||||
|             val date = Calendar.getInstance() | ||||
|             date.set(struct["startDate"]["year"].nullInt ?: 0, (struct["startDate"]["month"].nullInt | ||||
|                     ?: 0) - 1, | ||||
|                     struct["startDate"]["day"].nullInt ?: 0) | ||||
|             date.set( | ||||
|                 struct["startDate"]["year"].nullInt ?: 0, | ||||
|                 ( | ||||
|                     struct["startDate"]["month"].nullInt | ||||
|                         ?: 0 | ||||
|                     ) - 1, | ||||
|                 struct["startDate"]["day"].nullInt ?: 0 | ||||
|             ) | ||||
|             date.timeInMillis | ||||
|         } catch (_: Exception) { | ||||
|             0L | ||||
|         } | ||||
|  | ||||
|         return ALManga(struct["id"].asInt, struct["title"]["romaji"].asString, struct["coverImage"]["large"].asString, | ||||
|                 struct["description"].nullString.orEmpty(), struct["type"].asString, struct["status"].asString, | ||||
|                 date, struct["chapters"].nullInt ?: 0) | ||||
|         return ALManga( | ||||
|             struct["id"].asInt, struct["title"]["romaji"].asString, struct["coverImage"]["large"].asString, | ||||
|             struct["description"].nullString.orEmpty(), struct["type"].asString, struct["status"].asString, | ||||
|             date, struct["chapters"].nullInt ?: 0 | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     private fun jsonToALUserManga(struct: JsonObject): ALUserManga { | ||||
| @@ -280,8 +292,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { | ||||
|         } | ||||
|  | ||||
|         fun authUrl() = Uri.parse("${baseUrl}oauth/authorize").buildUpon() | ||||
|                 .appendQueryParameter("client_id", clientId) | ||||
|                 .appendQueryParameter("response_type", "token") | ||||
|                 .build() | ||||
|             .appendQueryParameter("client_id", clientId) | ||||
|             .appendQueryParameter("response_type", "token") | ||||
|             .build() | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -38,8 +38,8 @@ class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Int | ||||
|  | ||||
|         // Add the authorization header to the original request. | ||||
|         val authRequest = originalRequest.newBuilder() | ||||
|                 .addHeader("Authorization", "Bearer ${oauth!!.access_token}") | ||||
|                 .build() | ||||
|             .addHeader("Authorization", "Bearer ${oauth!!.access_token}") | ||||
|             .build() | ||||
|  | ||||
|         return chain.proceed(authRequest) | ||||
|     } | ||||
|   | ||||
| @@ -39,23 +39,23 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) { | ||||
|  | ||||
|     override fun bind(track: Track): Observable<Track> { | ||||
|         return api.statusLibManga(track) | ||||
|                 .flatMap { | ||||
|                     api.findLibManga(track).flatMap { remoteTrack -> | ||||
|                         if (remoteTrack != null && it != null) { | ||||
|                             track.copyPersonalFrom(remoteTrack) | ||||
|                             track.library_id = remoteTrack.library_id | ||||
|                             track.status = remoteTrack.status | ||||
|                             track.last_chapter_read = remoteTrack.last_chapter_read | ||||
|                             refresh(track) | ||||
|                         } else { | ||||
|                             // Set default fields if it's not found in the list | ||||
|                             track.score = DEFAULT_SCORE.toFloat() | ||||
|                             track.status = DEFAULT_STATUS | ||||
|                             add(track) | ||||
|                             update(track) | ||||
|                         } | ||||
|             .flatMap { | ||||
|                 api.findLibManga(track).flatMap { remoteTrack -> | ||||
|                     if (remoteTrack != null && it != null) { | ||||
|                         track.copyPersonalFrom(remoteTrack) | ||||
|                         track.library_id = remoteTrack.library_id | ||||
|                         track.status = remoteTrack.status | ||||
|                         track.last_chapter_read = remoteTrack.last_chapter_read | ||||
|                         refresh(track) | ||||
|                     } else { | ||||
|                         // Set default fields if it's not found in the list | ||||
|                         track.score = DEFAULT_SCORE.toFloat() | ||||
|                         track.status = DEFAULT_STATUS | ||||
|                         add(track) | ||||
|                         update(track) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     override fun search(query: String): Observable<List<TrackSearch>> { | ||||
| @@ -64,17 +64,17 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) { | ||||
|  | ||||
|     override fun refresh(track: Track): Observable<Track> { | ||||
|         return api.statusLibManga(track) | ||||
|                 .flatMap { | ||||
|                     track.copyPersonalFrom(it!!) | ||||
|                     api.findLibManga(track) | ||||
|                             .map { remoteTrack -> | ||||
|                                 if (remoteTrack != null) { | ||||
|                                     track.total_chapters = remoteTrack.total_chapters | ||||
|                                     track.status = remoteTrack.status | ||||
|                                 } | ||||
|                                 track | ||||
|                             } | ||||
|                 } | ||||
|             .flatMap { | ||||
|                 track.copyPersonalFrom(it!!) | ||||
|                 api.findLibManga(track) | ||||
|                     .map { remoteTrack -> | ||||
|                         if (remoteTrack != null) { | ||||
|                             track.total_chapters = remoteTrack.total_chapters | ||||
|                             track.status = remoteTrack.status | ||||
|                         } | ||||
|                         track | ||||
|                     } | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     override fun getLogo() = R.drawable.ic_tracker_bangumi | ||||
|   | ||||
| @@ -26,73 +26,74 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept | ||||
|  | ||||
|     fun addLibManga(track: Track): Observable<Track> { | ||||
|         val body = FormBody.Builder() | ||||
|                 .add("rating", track.score.toInt().toString()) | ||||
|                 .add("status", track.toBangumiStatus()) | ||||
|                 .build() | ||||
|             .add("rating", track.score.toInt().toString()) | ||||
|             .add("status", track.toBangumiStatus()) | ||||
|             .build() | ||||
|         val request = Request.Builder() | ||||
|                 .url("$apiUrl/collection/${track.media_id}/update") | ||||
|                 .post(body) | ||||
|                 .build() | ||||
|             .url("$apiUrl/collection/${track.media_id}/update") | ||||
|             .post(body) | ||||
|             .build() | ||||
|         return authClient.newCall(request) | ||||
|                 .asObservableSuccess() | ||||
|                 .map { | ||||
|                     track | ||||
|                 } | ||||
|             .asObservableSuccess() | ||||
|             .map { | ||||
|                 track | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     fun updateLibManga(track: Track): Observable<Track> { | ||||
|         // chapter update | ||||
|         val body = FormBody.Builder() | ||||
|                 .add("watched_eps", track.last_chapter_read.toString()) | ||||
|                 .build() | ||||
|             .add("watched_eps", track.last_chapter_read.toString()) | ||||
|             .build() | ||||
|         val request = Request.Builder() | ||||
|                 .url("$apiUrl/subject/${track.media_id}/update/watched_eps") | ||||
|                 .post(body) | ||||
|                 .build() | ||||
|             .url("$apiUrl/subject/${track.media_id}/update/watched_eps") | ||||
|             .post(body) | ||||
|             .build() | ||||
|  | ||||
|         // read status update | ||||
|         val sbody = FormBody.Builder() | ||||
|                 .add("status", track.toBangumiStatus()) | ||||
|                 .build() | ||||
|             .add("status", track.toBangumiStatus()) | ||||
|             .build() | ||||
|         val srequest = Request.Builder() | ||||
|                 .url("$apiUrl/collection/${track.media_id}/update") | ||||
|                 .post(sbody) | ||||
|                 .build() | ||||
|             .url("$apiUrl/collection/${track.media_id}/update") | ||||
|             .post(sbody) | ||||
|             .build() | ||||
|         return authClient.newCall(srequest) | ||||
|                 .asObservableSuccess() | ||||
|                 .map { | ||||
|                     track | ||||
|                 }.flatMap { | ||||
|                     authClient.newCall(request) | ||||
|                             .asObservableSuccess() | ||||
|                             .map { | ||||
|                                 track | ||||
|                             } | ||||
|                 } | ||||
|             .asObservableSuccess() | ||||
|             .map { | ||||
|                 track | ||||
|             }.flatMap { | ||||
|                 authClient.newCall(request) | ||||
|                     .asObservableSuccess() | ||||
|                     .map { | ||||
|                         track | ||||
|                     } | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     fun search(search: String): Observable<List<TrackSearch>> { | ||||
|         val url = Uri.parse( | ||||
|                 "$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}").buildUpon() | ||||
|                 .appendQueryParameter("max_results", "20") | ||||
|                 .build() | ||||
|             "$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}" | ||||
|         ).buildUpon() | ||||
|             .appendQueryParameter("max_results", "20") | ||||
|             .build() | ||||
|         val request = Request.Builder() | ||||
|                 .url(url.toString()) | ||||
|                 .get() | ||||
|                 .build() | ||||
|             .url(url.toString()) | ||||
|             .get() | ||||
|             .build() | ||||
|         return authClient.newCall(request) | ||||
|                 .asObservableSuccess() | ||||
|                 .map { netResponse -> | ||||
|                     var responseBody = netResponse.body?.string().orEmpty() | ||||
|                     if (responseBody.isEmpty()) { | ||||
|                         throw Exception("Null Response") | ||||
|                     } | ||||
|                     if (responseBody.contains("\"code\":404")) { | ||||
|                         responseBody = "{\"results\":0,\"list\":[]}" | ||||
|                     } | ||||
|                     val response = JsonParser.parseString(responseBody).obj["list"]?.array | ||||
|                     response?.filter { it.obj["type"].asInt == 1 }?.map { jsonToSearch(it.obj) } | ||||
|             .asObservableSuccess() | ||||
|             .map { netResponse -> | ||||
|                 var responseBody = netResponse.body?.string().orEmpty() | ||||
|                 if (responseBody.isEmpty()) { | ||||
|                     throw Exception("Null Response") | ||||
|                 } | ||||
|                 if (responseBody.contains("\"code\":404")) { | ||||
|                     responseBody = "{\"results\":0,\"list\":[]}" | ||||
|                 } | ||||
|                 val response = JsonParser.parseString(responseBody).obj["list"]?.array | ||||
|                 response?.filter { it.obj["type"].asInt == 1 }?.map { jsonToSearch(it.obj) } | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     private fun jsonToSearch(obj: JsonObject): TrackSearch { | ||||
| @@ -109,9 +110,15 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept | ||||
|         return Track.create(TrackManager.BANGUMI).apply { | ||||
|             title = mangas["name"].asString | ||||
|             media_id = mangas["id"].asInt | ||||
|             score = if (mangas["rating"] != null) | ||||
|                 (if (mangas["rating"].isJsonObject) mangas["rating"].obj["score"].asFloat else 0f) | ||||
|             else 0f | ||||
|             score = if (mangas["rating"] != null) { | ||||
|                 if (mangas["rating"].isJsonObject) { | ||||
|                     mangas["rating"].obj["score"].asFloat | ||||
|                 } else { | ||||
|                     0f | ||||
|                 } | ||||
|             } else { | ||||
|                 0f | ||||
|             } | ||||
|             status = Bangumi.DEFAULT_STATUS | ||||
|             tracking_url = mangas["url"].asString | ||||
|         } | ||||
| @@ -120,37 +127,37 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept | ||||
|     fun findLibManga(track: Track): Observable<Track?> { | ||||
|         val urlMangas = "$apiUrl/subject/${track.media_id}" | ||||
|         val requestMangas = Request.Builder() | ||||
|                 .url(urlMangas) | ||||
|                 .get() | ||||
|                 .build() | ||||
|             .url(urlMangas) | ||||
|             .get() | ||||
|             .build() | ||||
|  | ||||
|         return authClient.newCall(requestMangas) | ||||
|                 .asObservableSuccess() | ||||
|                 .map { netResponse -> | ||||
|                     // get comic info | ||||
|                     val responseBody = netResponse.body?.string().orEmpty() | ||||
|                     jsonToTrack(JsonParser.parseString(responseBody).obj) | ||||
|                 } | ||||
|             .asObservableSuccess() | ||||
|             .map { netResponse -> | ||||
|                 // get comic info | ||||
|                 val responseBody = netResponse.body?.string().orEmpty() | ||||
|                 jsonToTrack(JsonParser.parseString(responseBody).obj) | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     fun statusLibManga(track: Track): Observable<Track?> { | ||||
|         val urlUserRead = "$apiUrl/collection/${track.media_id}" | ||||
|         val requestUserRead = Request.Builder() | ||||
|                 .url(urlUserRead) | ||||
|                 .cacheControl(CacheControl.FORCE_NETWORK) | ||||
|                 .get() | ||||
|                 .build() | ||||
|             .url(urlUserRead) | ||||
|             .cacheControl(CacheControl.FORCE_NETWORK) | ||||
|             .get() | ||||
|             .build() | ||||
|  | ||||
|         // todo get user readed chapter here | ||||
|         return authClient.newCall(requestUserRead) | ||||
|                 .asObservableSuccess() | ||||
|                 .map { netResponse -> | ||||
|                     val resp = netResponse.body?.string() | ||||
|                     val coll = gson.fromJson(resp, Collection::class.java) | ||||
|                     track.status = coll.status?.id!! | ||||
|                     track.last_chapter_read = coll.ep_status!! | ||||
|                     track | ||||
|                 } | ||||
|             .asObservableSuccess() | ||||
|             .map { netResponse -> | ||||
|                 val resp = netResponse.body?.string() | ||||
|                 val coll = gson.fromJson(resp, Collection::class.java) | ||||
|                 track.status = coll.status?.id!! | ||||
|                 track.last_chapter_read = coll.ep_status!! | ||||
|                 track | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     fun accessToken(code: String): Observable<OAuth> { | ||||
| @@ -163,14 +170,15 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun accessTokenRequest(code: String) = POST(oauthUrl, | ||||
|             body = FormBody.Builder() | ||||
|                     .add("grant_type", "authorization_code") | ||||
|                     .add("client_id", clientId) | ||||
|                     .add("client_secret", clientSecret) | ||||
|                     .add("code", code) | ||||
|                     .add("redirect_uri", redirectUrl) | ||||
|                     .build() | ||||
|     private fun accessTokenRequest(code: String) = POST( | ||||
|         oauthUrl, | ||||
|         body = FormBody.Builder() | ||||
|             .add("grant_type", "authorization_code") | ||||
|             .add("client_id", clientId) | ||||
|             .add("client_secret", clientSecret) | ||||
|             .add("code", code) | ||||
|             .add("redirect_uri", redirectUrl) | ||||
|             .build() | ||||
|     ) | ||||
|  | ||||
|     companion object { | ||||
| @@ -190,19 +198,21 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept | ||||
|         } | ||||
|  | ||||
|         fun authUrl() = | ||||
|                 Uri.parse(loginUrl).buildUpon() | ||||
|                         .appendQueryParameter("client_id", clientId) | ||||
|                         .appendQueryParameter("response_type", "code") | ||||
|                         .appendQueryParameter("redirect_uri", redirectUrl) | ||||
|                         .build() | ||||
|             Uri.parse(loginUrl).buildUpon() | ||||
|                 .appendQueryParameter("client_id", clientId) | ||||
|                 .appendQueryParameter("response_type", "code") | ||||
|                 .appendQueryParameter("redirect_uri", redirectUrl) | ||||
|                 .build() | ||||
|  | ||||
|         fun refreshTokenRequest(token: String) = POST(oauthUrl, | ||||
|                 body = FormBody.Builder() | ||||
|                         .add("grant_type", "refresh_token") | ||||
|                         .add("client_id", clientId) | ||||
|                         .add("client_secret", clientSecret) | ||||
|                         .add("refresh_token", token) | ||||
|                         .add("redirect_uri", redirectUrl) | ||||
|                         .build()) | ||||
|         fun refreshTokenRequest(token: String) = POST( | ||||
|             oauthUrl, | ||||
|             body = FormBody.Builder() | ||||
|                 .add("grant_type", "refresh_token") | ||||
|                 .add("client_id", clientId) | ||||
|                 .add("client_secret", clientSecret) | ||||
|                 .add("refresh_token", token) | ||||
|                 .add("redirect_uri", redirectUrl) | ||||
|                 .build() | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -36,25 +36,28 @@ class BangumiInterceptor(val bangumi: Bangumi, val gson: Gson) : Interceptor { | ||||
|         } | ||||
|  | ||||
|         val authRequest = if (originalRequest.method == "GET") originalRequest.newBuilder() | ||||
|                 .header("User-Agent", "Tachiyomi") | ||||
|                 .url(originalRequest.url.newBuilder() | ||||
|                         .addQueryParameter("access_token", currAuth.access_token).build()) | ||||
|                 .build() else originalRequest.newBuilder() | ||||
|                 .post(addTocken(currAuth.access_token, originalRequest.body as FormBody)) | ||||
|                 .header("User-Agent", "Tachiyomi") | ||||
|                 .build() | ||||
|             .header("User-Agent", "Tachiyomi") | ||||
|             .url( | ||||
|                 originalRequest.url.newBuilder() | ||||
|                     .addQueryParameter("access_token", currAuth.access_token).build() | ||||
|             ) | ||||
|             .build() else originalRequest.newBuilder() | ||||
|             .post(addTocken(currAuth.access_token, originalRequest.body as FormBody)) | ||||
|             .header("User-Agent", "Tachiyomi") | ||||
|             .build() | ||||
|  | ||||
|         return chain.proceed(authRequest) | ||||
|     } | ||||
|  | ||||
|     fun newAuth(oauth: OAuth?) { | ||||
|         this.oauth = if (oauth == null) null else OAuth( | ||||
|                 oauth.access_token, | ||||
|                 oauth.token_type, | ||||
|                 System.currentTimeMillis() / 1000, | ||||
|                 oauth.expires_in, | ||||
|                 oauth.refresh_token, | ||||
|                 this.oauth?.user_id) | ||||
|             oauth.access_token, | ||||
|             oauth.token_type, | ||||
|             System.currentTimeMillis() / 1000, | ||||
|             oauth.expires_in, | ||||
|             oauth.refresh_token, | ||||
|             this.oauth?.user_id | ||||
|         ) | ||||
|  | ||||
|         bangumi.saveToken(oauth) | ||||
|     } | ||||
|   | ||||
| @@ -78,17 +78,17 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) { | ||||
|  | ||||
|     override fun bind(track: Track): Observable<Track> { | ||||
|         return api.findLibManga(track, getUserId()) | ||||
|                 .flatMap { remoteTrack -> | ||||
|                     if (remoteTrack != null) { | ||||
|                         track.copyPersonalFrom(remoteTrack) | ||||
|                         track.media_id = remoteTrack.media_id | ||||
|                         update(track) | ||||
|                     } else { | ||||
|                         track.score = DEFAULT_SCORE | ||||
|                         track.status = DEFAULT_STATUS | ||||
|                         add(track) | ||||
|                     } | ||||
|             .flatMap { remoteTrack -> | ||||
|                 if (remoteTrack != null) { | ||||
|                     track.copyPersonalFrom(remoteTrack) | ||||
|                     track.media_id = remoteTrack.media_id | ||||
|                     update(track) | ||||
|                 } else { | ||||
|                     track.score = DEFAULT_SCORE | ||||
|                     track.status = DEFAULT_STATUS | ||||
|                     add(track) | ||||
|                 } | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     override fun search(query: String): Observable<List<TrackSearch>> { | ||||
| @@ -97,20 +97,20 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) { | ||||
|  | ||||
|     override fun refresh(track: Track): Observable<Track> { | ||||
|         return api.getLibManga(track) | ||||
|                 .map { remoteTrack -> | ||||
|                     track.copyPersonalFrom(remoteTrack) | ||||
|                     track.total_chapters = remoteTrack.total_chapters | ||||
|                     track | ||||
|                 } | ||||
|             .map { remoteTrack -> | ||||
|                 track.copyPersonalFrom(remoteTrack) | ||||
|                 track.total_chapters = remoteTrack.total_chapters | ||||
|                 track | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     override fun login(username: String, password: String): Completable { | ||||
|         return api.login(username, password) | ||||
|                 .doOnNext { interceptor.newAuth(it) } | ||||
|                 .flatMap { api.getCurrentUser() } | ||||
|                 .doOnNext { userId -> saveCredentials(username, userId) } | ||||
|                 .doOnError { logout() } | ||||
|                 .toCompletable() | ||||
|             .doOnNext { interceptor.newAuth(it) } | ||||
|             .flatMap { api.getCurrentUser() } | ||||
|             .doOnNext { userId -> saveCredentials(username, userId) } | ||||
|             .doOnError { logout() } | ||||
|             .toCompletable() | ||||
|     } | ||||
|  | ||||
|     override fun logout() { | ||||
|   | ||||
| @@ -33,59 +33,59 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) | ||||
|     private val authClient = client.newBuilder().addInterceptor(interceptor).build() | ||||
|  | ||||
|     private val rest = Retrofit.Builder() | ||||
|             .baseUrl(baseUrl) | ||||
|             .client(authClient) | ||||
|             .addConverterFactory(GsonConverterFactory.create(GsonBuilder().serializeNulls().create())) | ||||
|             .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) | ||||
|             .build() | ||||
|             .create(Rest::class.java) | ||||
|         .baseUrl(baseUrl) | ||||
|         .client(authClient) | ||||
|         .addConverterFactory(GsonConverterFactory.create(GsonBuilder().serializeNulls().create())) | ||||
|         .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) | ||||
|         .build() | ||||
|         .create(Rest::class.java) | ||||
|  | ||||
|     private val searchRest = Retrofit.Builder() | ||||
|             .baseUrl(algoliaKeyUrl) | ||||
|             .client(authClient) | ||||
|             .addConverterFactory(GsonConverterFactory.create()) | ||||
|             .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) | ||||
|             .build() | ||||
|             .create(SearchKeyRest::class.java) | ||||
|         .baseUrl(algoliaKeyUrl) | ||||
|         .client(authClient) | ||||
|         .addConverterFactory(GsonConverterFactory.create()) | ||||
|         .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) | ||||
|         .build() | ||||
|         .create(SearchKeyRest::class.java) | ||||
|  | ||||
|     private val algoliaRest = Retrofit.Builder() | ||||
|             .baseUrl(algoliaUrl) | ||||
|             .client(client) | ||||
|             .addConverterFactory(GsonConverterFactory.create()) | ||||
|             .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) | ||||
|             .build() | ||||
|             .create(AgoliaSearchRest::class.java) | ||||
|         .baseUrl(algoliaUrl) | ||||
|         .client(client) | ||||
|         .addConverterFactory(GsonConverterFactory.create()) | ||||
|         .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) | ||||
|         .build() | ||||
|         .create(AgoliaSearchRest::class.java) | ||||
|  | ||||
|     fun addLibManga(track: Track, userId: String): Observable<Track> { | ||||
|         return Observable.defer { | ||||
|             // @formatter:off | ||||
|             val data = jsonObject( | ||||
|                     "type" to "libraryEntries", | ||||
|                     "attributes" to jsonObject( | ||||
|                             "status" to track.toKitsuStatus(), | ||||
|                             "progress" to track.last_chapter_read | ||||
|                 "type" to "libraryEntries", | ||||
|                 "attributes" to jsonObject( | ||||
|                     "status" to track.toKitsuStatus(), | ||||
|                     "progress" to track.last_chapter_read | ||||
|                 ), | ||||
|                 "relationships" to jsonObject( | ||||
|                     "user" to jsonObject( | ||||
|                         "data" to jsonObject( | ||||
|                             "id" to userId, | ||||
|                             "type" to "users" | ||||
|                         ) | ||||
|                     ), | ||||
|                     "relationships" to jsonObject( | ||||
|                             "user" to jsonObject( | ||||
|                                     "data" to jsonObject( | ||||
|                                             "id" to userId, | ||||
|                                             "type" to "users" | ||||
|                                     ) | ||||
|                             ), | ||||
|                             "media" to jsonObject( | ||||
|                                     "data" to jsonObject( | ||||
|                                             "id" to track.media_id, | ||||
|                                             "type" to "manga" | ||||
|                                     ) | ||||
|                             ) | ||||
|                     "media" to jsonObject( | ||||
|                         "data" to jsonObject( | ||||
|                             "id" to track.media_id, | ||||
|                             "type" to "manga" | ||||
|                         ) | ||||
|                     ) | ||||
|                 ) | ||||
|             ) | ||||
|  | ||||
|             rest.addLibManga(jsonObject("data" to data)) | ||||
|                     .map { json -> | ||||
|                         track.media_id = json["data"]["id"].int | ||||
|                         track | ||||
|                     } | ||||
|                 .map { json -> | ||||
|                     track.media_id = json["data"]["id"].int | ||||
|                     track | ||||
|                 } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -93,77 +93,77 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) | ||||
|         return Observable.defer { | ||||
|             // @formatter:off | ||||
|             val data = jsonObject( | ||||
|                     "type" to "libraryEntries", | ||||
|                     "id" to track.media_id, | ||||
|                     "attributes" to jsonObject( | ||||
|                             "status" to track.toKitsuStatus(), | ||||
|                             "progress" to track.last_chapter_read, | ||||
|                             "ratingTwenty" to track.toKitsuScore() | ||||
|                     ) | ||||
|                 "type" to "libraryEntries", | ||||
|                 "id" to track.media_id, | ||||
|                 "attributes" to jsonObject( | ||||
|                     "status" to track.toKitsuStatus(), | ||||
|                     "progress" to track.last_chapter_read, | ||||
|                     "ratingTwenty" to track.toKitsuScore() | ||||
|                 ) | ||||
|             ) | ||||
|             // @formatter:on | ||||
|  | ||||
|             rest.updateLibManga(track.media_id, jsonObject("data" to data)) | ||||
|                     .map { track } | ||||
|                 .map { track } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun search(query: String): Observable<List<TrackSearch>> { | ||||
|         return searchRest | ||||
|                 .getKey().map { json -> | ||||
|                     json["media"].asJsonObject["key"].string | ||||
|                 }.flatMap { key -> | ||||
|                     algoliaSearch(key, query) | ||||
|                 } | ||||
|             .getKey().map { json -> | ||||
|                 json["media"].asJsonObject["key"].string | ||||
|             }.flatMap { key -> | ||||
|                 algoliaSearch(key, query) | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     private fun algoliaSearch(key: String, query: String): Observable<List<TrackSearch>> { | ||||
|         val jsonObject = jsonObject("params" to "query=$query$algoliaFilter") | ||||
|         return algoliaRest | ||||
|                 .getSearchQuery(algoliaAppId, key, jsonObject) | ||||
|                 .map { json -> | ||||
|                     val data = json["hits"].array | ||||
|                     data.map { KitsuSearchManga(it.obj) } | ||||
|                             .filter { it.subType != "novel" } | ||||
|                             .map { it.toTrack() } | ||||
|                 } | ||||
|             .getSearchQuery(algoliaAppId, key, jsonObject) | ||||
|             .map { json -> | ||||
|                 val data = json["hits"].array | ||||
|                 data.map { KitsuSearchManga(it.obj) } | ||||
|                     .filter { it.subType != "novel" } | ||||
|                     .map { it.toTrack() } | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     fun findLibManga(track: Track, userId: String): Observable<Track?> { | ||||
|         return rest.findLibManga(track.media_id, userId) | ||||
|                 .map { json -> | ||||
|                     val data = json["data"].array | ||||
|                     if (data.size() > 0) { | ||||
|                         val manga = json["included"].array[0].obj | ||||
|                         KitsuLibManga(data[0].obj, manga).toTrack() | ||||
|                     } else { | ||||
|                         null | ||||
|                     } | ||||
|             .map { json -> | ||||
|                 val data = json["data"].array | ||||
|                 if (data.size() > 0) { | ||||
|                     val manga = json["included"].array[0].obj | ||||
|                     KitsuLibManga(data[0].obj, manga).toTrack() | ||||
|                 } else { | ||||
|                     null | ||||
|                 } | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     fun getLibManga(track: Track): Observable<Track> { | ||||
|         return rest.getLibManga(track.media_id) | ||||
|                 .map { json -> | ||||
|                     val data = json["data"].array | ||||
|                     if (data.size() > 0) { | ||||
|                         val manga = json["included"].array[0].obj | ||||
|                         KitsuLibManga(data[0].obj, manga).toTrack() | ||||
|                     } else { | ||||
|                         throw Exception("Could not find manga") | ||||
|                     } | ||||
|             .map { json -> | ||||
|                 val data = json["data"].array | ||||
|                 if (data.size() > 0) { | ||||
|                     val manga = json["included"].array[0].obj | ||||
|                     KitsuLibManga(data[0].obj, manga).toTrack() | ||||
|                 } else { | ||||
|                     throw Exception("Could not find manga") | ||||
|                 } | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     fun login(username: String, password: String): Observable<OAuth> { | ||||
|         return Retrofit.Builder() | ||||
|                 .baseUrl(loginUrl) | ||||
|                 .client(client) | ||||
|                 .addConverterFactory(GsonConverterFactory.create()) | ||||
|                 .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) | ||||
|                 .build() | ||||
|                 .create(LoginRest::class.java) | ||||
|                 .requestAccessToken(username, password) | ||||
|             .baseUrl(loginUrl) | ||||
|             .client(client) | ||||
|             .addConverterFactory(GsonConverterFactory.create()) | ||||
|             .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) | ||||
|             .build() | ||||
|             .create(LoginRest::class.java) | ||||
|             .requestAccessToken(username, password) | ||||
|     } | ||||
|  | ||||
|     fun getCurrentUser(): Observable<String> { | ||||
| @@ -242,12 +242,14 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) | ||||
|             return baseMangaUrl + remoteId | ||||
|         } | ||||
|  | ||||
|         fun refreshTokenRequest(token: String) = POST("${loginUrl}oauth/token", | ||||
|                 body = FormBody.Builder() | ||||
|                         .add("grant_type", "refresh_token") | ||||
|                         .add("client_id", clientId) | ||||
|                         .add("client_secret", clientSecret) | ||||
|                         .add("refresh_token", token) | ||||
|                         .build()) | ||||
|         fun refreshTokenRequest(token: String) = POST( | ||||
|             "${loginUrl}oauth/token", | ||||
|             body = FormBody.Builder() | ||||
|                 .add("grant_type", "refresh_token") | ||||
|                 .add("client_id", clientId) | ||||
|                 .add("client_secret", clientSecret) | ||||
|                 .add("refresh_token", token) | ||||
|                 .build() | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -30,10 +30,10 @@ class KitsuInterceptor(val kitsu: Kitsu, val gson: Gson) : Interceptor { | ||||
|  | ||||
|         // Add the authorization header to the original request. | ||||
|         val authRequest = originalRequest.newBuilder() | ||||
|                 .addHeader("Authorization", "Bearer ${oauth!!.access_token}") | ||||
|                 .header("Accept", "application/vnd.api+json") | ||||
|                 .header("Content-Type", "application/vnd.api+json") | ||||
|                 .build() | ||||
|             .addHeader("Authorization", "Bearer ${oauth!!.access_token}") | ||||
|             .header("Accept", "application/vnd.api+json") | ||||
|             .header("Content-Type", "application/vnd.api+json") | ||||
|             .build() | ||||
|  | ||||
|         return chain.proceed(authRequest) | ||||
|     } | ||||
|   | ||||
| @@ -74,17 +74,17 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) { | ||||
|  | ||||
|     override fun bind(track: Track): Observable<Track> { | ||||
|         return api.findLibManga(track) | ||||
|                 .flatMap { remoteTrack -> | ||||
|                     if (remoteTrack != null) { | ||||
|                         track.copyPersonalFrom(remoteTrack) | ||||
|                         update(track) | ||||
|                     } else { | ||||
|                         // Set default fields if it's not found in the list | ||||
|                         track.score = DEFAULT_SCORE.toFloat() | ||||
|                         track.status = DEFAULT_STATUS | ||||
|                         add(track) | ||||
|                     } | ||||
|             .flatMap { remoteTrack -> | ||||
|                 if (remoteTrack != null) { | ||||
|                     track.copyPersonalFrom(remoteTrack) | ||||
|                     update(track) | ||||
|                 } else { | ||||
|                     // Set default fields if it's not found in the list | ||||
|                     track.score = DEFAULT_SCORE.toFloat() | ||||
|                     track.status = DEFAULT_STATUS | ||||
|                     add(track) | ||||
|                 } | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     override fun search(query: String): Observable<List<TrackSearch>> { | ||||
| @@ -93,21 +93,21 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) { | ||||
|  | ||||
|     override fun refresh(track: Track): Observable<Track> { | ||||
|         return api.getLibManga(track) | ||||
|                 .map { remoteTrack -> | ||||
|                     track.copyPersonalFrom(remoteTrack) | ||||
|                     track.total_chapters = remoteTrack.total_chapters | ||||
|                     track | ||||
|                 } | ||||
|             .map { remoteTrack -> | ||||
|                 track.copyPersonalFrom(remoteTrack) | ||||
|                 track.total_chapters = remoteTrack.total_chapters | ||||
|                 track | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     override fun login(username: String, password: String): Completable { | ||||
|         logout() | ||||
|  | ||||
|         return Observable.fromCallable { api.login(username, password) } | ||||
|                 .doOnNext { csrf -> saveCSRF(csrf) } | ||||
|                 .doOnNext { saveCredentials(username, password) } | ||||
|                 .doOnError { logout() } | ||||
|                 .toCompletable() | ||||
|             .doOnNext { csrf -> saveCSRF(csrf) } | ||||
|             .doOnNext { saveCredentials(username, password) } | ||||
|             .doOnError { logout() } | ||||
|             .toCompletable() | ||||
|     } | ||||
|  | ||||
|     fun refreshLogin() { | ||||
| @@ -141,8 +141,8 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) { | ||||
|  | ||||
|     val isAuthorized: Boolean | ||||
|         get() = super.isLogged && | ||||
|                 getCSRF().isNotEmpty() && | ||||
|                 checkCookies() | ||||
|             getCSRF().isNotEmpty() && | ||||
|             checkCookies() | ||||
|  | ||||
|     fun getCSRF(): String = preferences.trackToken(this).get() | ||||
|  | ||||
| @@ -152,8 +152,9 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) { | ||||
|         var ckCount = 0 | ||||
|         val url = BASE_URL.toHttpUrlOrNull()!! | ||||
|         for (ck in networkService.cookieManager.get(url)) { | ||||
|             if (ck.name == USER_SESSION_COOKIE || ck.name == LOGGED_IN_COOKIE) | ||||
|             if (ck.name == USER_SESSION_COOKIE || ck.name == LOGGED_IN_COOKIE) { | ||||
|                 ckCount++ | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return ckCount == 2 | ||||
|   | ||||
| @@ -39,43 +39,45 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI | ||||
|         return if (query.startsWith(PREFIX_MY)) { | ||||
|             val realQuery = query.removePrefix(PREFIX_MY) | ||||
|             getList() | ||||
|                     .flatMap { Observable.from(it) } | ||||
|                     .filter { it.title.contains(realQuery, true) } | ||||
|                     .toList() | ||||
|                 .flatMap { Observable.from(it) } | ||||
|                 .filter { it.title.contains(realQuery, true) } | ||||
|                 .toList() | ||||
|         } else { | ||||
|             client.newCall(GET(searchUrl(query))) | ||||
|                     .asObservable() | ||||
|                     .flatMap { response -> | ||||
|                         Observable.from(Jsoup.parse(response.consumeBody()) | ||||
|                                 .select("div.js-categories-seasonal.js-block-list.list") | ||||
|                                 .select("table").select("tbody") | ||||
|                                 .select("tr").drop(1)) | ||||
|                 .asObservable() | ||||
|                 .flatMap { response -> | ||||
|                     Observable.from( | ||||
|                         Jsoup.parse(response.consumeBody()) | ||||
|                             .select("div.js-categories-seasonal.js-block-list.list") | ||||
|                             .select("table").select("tbody") | ||||
|                             .select("tr").drop(1) | ||||
|                     ) | ||||
|                 } | ||||
|                 .filter { row -> | ||||
|                     row.select(TD)[2].text() != "Novel" | ||||
|                 } | ||||
|                 .map { row -> | ||||
|                     TrackSearch.create(TrackManager.MYANIMELIST).apply { | ||||
|                         title = row.searchTitle() | ||||
|                         media_id = row.searchMediaId() | ||||
|                         total_chapters = row.searchTotalChapters() | ||||
|                         summary = row.searchSummary() | ||||
|                         cover_url = row.searchCoverUrl() | ||||
|                         tracking_url = mangaUrl(media_id) | ||||
|                         publishing_status = row.searchPublishingStatus() | ||||
|                         publishing_type = row.searchPublishingType() | ||||
|                         start_date = row.searchStartDate() | ||||
|                     } | ||||
|                     .filter { row -> | ||||
|                         row.select(TD)[2].text() != "Novel" | ||||
|                     } | ||||
|                     .map { row -> | ||||
|                         TrackSearch.create(TrackManager.MYANIMELIST).apply { | ||||
|                             title = row.searchTitle() | ||||
|                             media_id = row.searchMediaId() | ||||
|                             total_chapters = row.searchTotalChapters() | ||||
|                             summary = row.searchSummary() | ||||
|                             cover_url = row.searchCoverUrl() | ||||
|                             tracking_url = mangaUrl(media_id) | ||||
|                             publishing_status = row.searchPublishingStatus() | ||||
|                             publishing_type = row.searchPublishingType() | ||||
|                             start_date = row.searchStartDate() | ||||
|                         } | ||||
|                     } | ||||
|                     .toList() | ||||
|                 } | ||||
|                 .toList() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun addLibManga(track: Track): Observable<Track> { | ||||
|         return Observable.defer { | ||||
|             authClient.newCall(POST(url = addUrl(), body = mangaPostPayload(track))) | ||||
|                     .asObservableSuccess() | ||||
|                     .map { track } | ||||
|                 .asObservableSuccess() | ||||
|                 .map { track } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -95,40 +97,40 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI | ||||
|  | ||||
|             // Update remote | ||||
|             authClient.newCall(POST(url = editPageUrl(track.media_id), body = mangaEditPostBody(editData))) | ||||
|                     .asObservableSuccess() | ||||
|                     .map { | ||||
|                         track | ||||
|                     } | ||||
|                 .asObservableSuccess() | ||||
|                 .map { | ||||
|                     track | ||||
|                 } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun findLibManga(track: Track): Observable<Track?> { | ||||
|         return authClient.newCall(GET(url = editPageUrl(track.media_id))) | ||||
|                 .asObservable() | ||||
|                 .map { response -> | ||||
|                     var libTrack: Track? = null | ||||
|                     response.use { | ||||
|                         if (it.priorResponse?.isRedirect != true) { | ||||
|                             val trackForm = Jsoup.parse(it.consumeBody()) | ||||
|             .asObservable() | ||||
|             .map { response -> | ||||
|                 var libTrack: Track? = null | ||||
|                 response.use { | ||||
|                     if (it.priorResponse?.isRedirect != true) { | ||||
|                         val trackForm = Jsoup.parse(it.consumeBody()) | ||||
|  | ||||
|                             libTrack = Track.create(TrackManager.MYANIMELIST).apply { | ||||
|                                 last_chapter_read = trackForm.select("#add_manga_num_read_chapters").`val`().toInt() | ||||
|                                 total_chapters = trackForm.select("#totalChap").text().toInt() | ||||
|                                 status = trackForm.select("#add_manga_status > option[selected]").`val`().toInt() | ||||
|                                 score = trackForm.select("#add_manga_score > option[selected]").`val`().toFloatOrNull() | ||||
|                                         ?: 0f | ||||
|                                 started_reading_date = trackForm.searchDatePicker("#add_manga_start_date") | ||||
|                                 finished_reading_date = trackForm.searchDatePicker("#add_manga_finish_date") | ||||
|                             } | ||||
|                         libTrack = Track.create(TrackManager.MYANIMELIST).apply { | ||||
|                             last_chapter_read = trackForm.select("#add_manga_num_read_chapters").`val`().toInt() | ||||
|                             total_chapters = trackForm.select("#totalChap").text().toInt() | ||||
|                             status = trackForm.select("#add_manga_status > option[selected]").`val`().toInt() | ||||
|                             score = trackForm.select("#add_manga_score > option[selected]").`val`().toFloatOrNull() | ||||
|                                 ?: 0f | ||||
|                             started_reading_date = trackForm.searchDatePicker("#add_manga_start_date") | ||||
|                             finished_reading_date = trackForm.searchDatePicker("#add_manga_finish_date") | ||||
|                         } | ||||
|                     } | ||||
|                     libTrack | ||||
|                 } | ||||
|                 libTrack | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     fun getLibManga(track: Track): Observable<Track> { | ||||
|         return findLibManga(track) | ||||
|                 .map { it ?: throw Exception("Could not find manga") } | ||||
|             .map { it ?: throw Exception("Could not find manga") } | ||||
|     } | ||||
|  | ||||
|     fun login(username: String, password: String): String { | ||||
| @@ -143,8 +145,8 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI | ||||
|         val response = client.newCall(GET(loginUrl())).execute() | ||||
|  | ||||
|         return Jsoup.parse(response.consumeBody()) | ||||
|                 .select("meta[name=csrf_token]") | ||||
|                 .attr("content") | ||||
|             .select("meta[name=csrf_token]") | ||||
|             .attr("content") | ||||
|     } | ||||
|  | ||||
|     private fun login(username: String, password: String, csrf: String) { | ||||
| @@ -157,45 +159,45 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI | ||||
|  | ||||
|     private fun getList(): Observable<List<TrackSearch>> { | ||||
|         return getListUrl() | ||||
|                 .flatMap { url -> | ||||
|                     getListXml(url) | ||||
|             .flatMap { url -> | ||||
|                 getListXml(url) | ||||
|             } | ||||
|             .flatMap { doc -> | ||||
|                 Observable.from(doc.select("manga")) | ||||
|             } | ||||
|             .map { | ||||
|                 TrackSearch.create(TrackManager.MYANIMELIST).apply { | ||||
|                     title = it.selectText("manga_title")!! | ||||
|                     media_id = it.selectInt("manga_mangadb_id") | ||||
|                     last_chapter_read = it.selectInt("my_read_chapters") | ||||
|                     status = getStatus(it.selectText("my_status")!!) | ||||
|                     score = it.selectInt("my_score").toFloat() | ||||
|                     total_chapters = it.selectInt("manga_chapters") | ||||
|                     tracking_url = mangaUrl(media_id) | ||||
|                     started_reading_date = it.searchDateXml("my_start_date") | ||||
|                     finished_reading_date = it.searchDateXml("my_finish_date") | ||||
|                 } | ||||
|                 .flatMap { doc -> | ||||
|                     Observable.from(doc.select("manga")) | ||||
|                 } | ||||
|                 .map { | ||||
|                     TrackSearch.create(TrackManager.MYANIMELIST).apply { | ||||
|                         title = it.selectText("manga_title")!! | ||||
|                         media_id = it.selectInt("manga_mangadb_id") | ||||
|                         last_chapter_read = it.selectInt("my_read_chapters") | ||||
|                         status = getStatus(it.selectText("my_status")!!) | ||||
|                         score = it.selectInt("my_score").toFloat() | ||||
|                         total_chapters = it.selectInt("manga_chapters") | ||||
|                         tracking_url = mangaUrl(media_id) | ||||
|                         started_reading_date = it.searchDateXml("my_start_date") | ||||
|                         finished_reading_date = it.searchDateXml("my_finish_date") | ||||
|                     } | ||||
|                 } | ||||
|                 .toList() | ||||
|             } | ||||
|             .toList() | ||||
|     } | ||||
|  | ||||
|     private fun getListUrl(): Observable<String> { | ||||
|         return authClient.newCall(POST(url = exportListUrl(), body = exportPostBody())) | ||||
|                 .asObservable() | ||||
|                 .map { response -> | ||||
|                     baseUrl + Jsoup.parse(response.consumeBody()) | ||||
|                             .select("div.goodresult") | ||||
|                             .select("a") | ||||
|                             .attr("href") | ||||
|                 } | ||||
|             .asObservable() | ||||
|             .map { response -> | ||||
|                 baseUrl + Jsoup.parse(response.consumeBody()) | ||||
|                     .select("div.goodresult") | ||||
|                     .select("a") | ||||
|                     .attr("href") | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     private fun getListXml(url: String): Observable<Document> { | ||||
|         return authClient.newCall(GET(url)) | ||||
|                 .asObservable() | ||||
|                 .map { response -> | ||||
|                     Jsoup.parse(response.consumeXmlBody(), "", Parser.xmlParser()) | ||||
|                 } | ||||
|             .asObservable() | ||||
|             .map { response -> | ||||
|                 Jsoup.parse(response.consumeXmlBody(), "", Parser.xmlParser()) | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     private fun Response.consumeBody(): String? { | ||||
| @@ -222,28 +224,28 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI | ||||
|         val tables = page.select("form#main-form table") | ||||
|  | ||||
|         return MyAnimeListEditData( | ||||
|                 entry_id = tables[0].select("input[name=entry_id]").`val`(), // Always 0 | ||||
|                 manga_id = tables[0].select("#manga_id").`val`(), | ||||
|                 status = tables[0].select("#add_manga_status > option[selected]").`val`(), | ||||
|                 num_read_volumes = tables[0].select("#add_manga_num_read_volumes").`val`(), | ||||
|                 last_completed_vol = tables[0].select("input[name=last_completed_vol]").`val`(), // Always empty | ||||
|                 num_read_chapters = tables[0].select("#add_manga_num_read_chapters").`val`(), | ||||
|                 score = tables[0].select("#add_manga_score > option[selected]").`val`(), | ||||
|                 start_date_month = tables[0].select("#add_manga_start_date_month > option[selected]").`val`(), | ||||
|                 start_date_day = tables[0].select("#add_manga_start_date_day > option[selected]").`val`(), | ||||
|                 start_date_year = tables[0].select("#add_manga_start_date_year > option[selected]").`val`(), | ||||
|                 finish_date_month = tables[0].select("#add_manga_finish_date_month > option[selected]").`val`(), | ||||
|                 finish_date_day = tables[0].select("#add_manga_finish_date_day > option[selected]").`val`(), | ||||
|                 finish_date_year = tables[0].select("#add_manga_finish_date_year > option[selected]").`val`(), | ||||
|                 tags = tables[1].select("#add_manga_tags").`val`(), | ||||
|                 priority = tables[1].select("#add_manga_priority > option[selected]").`val`(), | ||||
|                 storage_type = tables[1].select("#add_manga_storage_type > option[selected]").`val`(), | ||||
|                 num_retail_volumes = tables[1].select("#add_manga_num_retail_volumes").`val`(), | ||||
|                 num_read_times = tables[1].select("#add_manga_num_read_times").`val`(), | ||||
|                 reread_value = tables[1].select("#add_manga_reread_value > option[selected]").`val`(), | ||||
|                 comments = tables[1].select("#add_manga_comments").`val`(), | ||||
|                 is_asked_to_discuss = tables[1].select("#add_manga_is_asked_to_discuss > option[selected]").`val`(), | ||||
|                 sns_post_type = tables[1].select("#add_manga_sns_post_type > option[selected]").`val`() | ||||
|             entry_id = tables[0].select("input[name=entry_id]").`val`(), // Always 0 | ||||
|             manga_id = tables[0].select("#manga_id").`val`(), | ||||
|             status = tables[0].select("#add_manga_status > option[selected]").`val`(), | ||||
|             num_read_volumes = tables[0].select("#add_manga_num_read_volumes").`val`(), | ||||
|             last_completed_vol = tables[0].select("input[name=last_completed_vol]").`val`(), // Always empty | ||||
|             num_read_chapters = tables[0].select("#add_manga_num_read_chapters").`val`(), | ||||
|             score = tables[0].select("#add_manga_score > option[selected]").`val`(), | ||||
|             start_date_month = tables[0].select("#add_manga_start_date_month > option[selected]").`val`(), | ||||
|             start_date_day = tables[0].select("#add_manga_start_date_day > option[selected]").`val`(), | ||||
|             start_date_year = tables[0].select("#add_manga_start_date_year > option[selected]").`val`(), | ||||
|             finish_date_month = tables[0].select("#add_manga_finish_date_month > option[selected]").`val`(), | ||||
|             finish_date_day = tables[0].select("#add_manga_finish_date_day > option[selected]").`val`(), | ||||
|             finish_date_year = tables[0].select("#add_manga_finish_date_year > option[selected]").`val`(), | ||||
|             tags = tables[1].select("#add_manga_tags").`val`(), | ||||
|             priority = tables[1].select("#add_manga_priority > option[selected]").`val`(), | ||||
|             storage_type = tables[1].select("#add_manga_storage_type > option[selected]").`val`(), | ||||
|             num_retail_volumes = tables[1].select("#add_manga_num_retail_volumes").`val`(), | ||||
|             num_read_times = tables[1].select("#add_manga_num_read_times").`val`(), | ||||
|             reread_value = tables[1].select("#add_manga_reread_value > option[selected]").`val`(), | ||||
|             comments = tables[1].select("#add_manga_comments").`val`(), | ||||
|             is_asked_to_discuss = tables[1].select("#add_manga_is_asked_to_discuss > option[selected]").`val`(), | ||||
|             sns_post_type = tables[1].select("#add_manga_sns_post_type > option[selected]").`val`() | ||||
|         ) | ||||
|     } | ||||
|  | ||||
| @@ -259,98 +261,99 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI | ||||
|         private fun mangaUrl(remoteId: Int) = baseMangaUrl + remoteId | ||||
|  | ||||
|         private fun loginUrl() = Uri.parse(baseUrl).buildUpon() | ||||
|                 .appendPath("login.php") | ||||
|                 .toString() | ||||
|             .appendPath("login.php") | ||||
|             .toString() | ||||
|  | ||||
|         private fun searchUrl(query: String): String { | ||||
|             val col = "c[]" | ||||
|             return Uri.parse(baseUrl).buildUpon() | ||||
|                     .appendPath("manga.php") | ||||
|                     .appendQueryParameter("q", query) | ||||
|                     .appendQueryParameter(col, "a") | ||||
|                     .appendQueryParameter(col, "b") | ||||
|                     .appendQueryParameter(col, "c") | ||||
|                     .appendQueryParameter(col, "d") | ||||
|                     .appendQueryParameter(col, "e") | ||||
|                     .appendQueryParameter(col, "g") | ||||
|                     .toString() | ||||
|                 .appendPath("manga.php") | ||||
|                 .appendQueryParameter("q", query) | ||||
|                 .appendQueryParameter(col, "a") | ||||
|                 .appendQueryParameter(col, "b") | ||||
|                 .appendQueryParameter(col, "c") | ||||
|                 .appendQueryParameter(col, "d") | ||||
|                 .appendQueryParameter(col, "e") | ||||
|                 .appendQueryParameter(col, "g") | ||||
|                 .toString() | ||||
|         } | ||||
|  | ||||
|         private fun exportListUrl() = Uri.parse(baseUrl).buildUpon() | ||||
|                 .appendPath("panel.php") | ||||
|                 .appendQueryParameter("go", "export") | ||||
|                 .toString() | ||||
|             .appendPath("panel.php") | ||||
|             .appendQueryParameter("go", "export") | ||||
|             .toString() | ||||
|  | ||||
|         private fun editPageUrl(mediaId: Int) = Uri.parse(baseModifyListUrl).buildUpon() | ||||
|                 .appendPath(mediaId.toString()) | ||||
|                 .appendPath("edit") | ||||
|                 .toString() | ||||
|             .appendPath(mediaId.toString()) | ||||
|             .appendPath("edit") | ||||
|             .toString() | ||||
|  | ||||
|         private fun addUrl() = Uri.parse(baseModifyListUrl).buildUpon() | ||||
|                 .appendPath("add.json") | ||||
|                 .toString() | ||||
|             .appendPath("add.json") | ||||
|             .toString() | ||||
|  | ||||
|         private fun loginPostBody(username: String, password: String, csrf: String): RequestBody { | ||||
|             return FormBody.Builder() | ||||
|                     .add("user_name", username) | ||||
|                     .add("password", password) | ||||
|                     .add("cookie", "1") | ||||
|                     .add("sublogin", "Login") | ||||
|                     .add("submit", "1") | ||||
|                     .add(CSRF, csrf) | ||||
|                     .build() | ||||
|                 .add("user_name", username) | ||||
|                 .add("password", password) | ||||
|                 .add("cookie", "1") | ||||
|                 .add("sublogin", "Login") | ||||
|                 .add("submit", "1") | ||||
|                 .add(CSRF, csrf) | ||||
|                 .build() | ||||
|         } | ||||
|  | ||||
|         private fun exportPostBody(): RequestBody { | ||||
|             return FormBody.Builder() | ||||
|                     .add("type", "2") | ||||
|                     .add("subexport", "Export My List") | ||||
|                     .build() | ||||
|                 .add("type", "2") | ||||
|                 .add("subexport", "Export My List") | ||||
|                 .build() | ||||
|         } | ||||
|  | ||||
|         private fun mangaPostPayload(track: Track): RequestBody { | ||||
|             val body = JSONObject() | ||||
|                     .put("manga_id", track.media_id) | ||||
|                     .put("status", track.status) | ||||
|                     .put("score", track.score) | ||||
|                     .put("num_read_chapters", track.last_chapter_read) | ||||
|                 .put("manga_id", track.media_id) | ||||
|                 .put("status", track.status) | ||||
|                 .put("score", track.score) | ||||
|                 .put("num_read_chapters", track.last_chapter_read) | ||||
|  | ||||
|             return body.toString().toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull()) | ||||
|         } | ||||
|  | ||||
|         private fun mangaEditPostBody(track: MyAnimeListEditData): RequestBody { | ||||
|             return FormBody.Builder() | ||||
|                     .add("entry_id", track.entry_id) | ||||
|                     .add("manga_id", track.manga_id) | ||||
|                     .add("add_manga[status]", track.status) | ||||
|                     .add("add_manga[num_read_volumes]", track.num_read_volumes) | ||||
|                     .add("last_completed_vol", track.last_completed_vol) | ||||
|                     .add("add_manga[num_read_chapters]", track.num_read_chapters) | ||||
|                     .add("add_manga[score]", track.score) | ||||
|                     .add("add_manga[start_date][month]", track.start_date_month) | ||||
|                     .add("add_manga[start_date][day]", track.start_date_day) | ||||
|                     .add("add_manga[start_date][year]", track.start_date_year) | ||||
|                     .add("add_manga[finish_date][month]", track.finish_date_month) | ||||
|                     .add("add_manga[finish_date][day]", track.finish_date_day) | ||||
|                     .add("add_manga[finish_date][year]", track.finish_date_year) | ||||
|                     .add("add_manga[tags]", track.tags) | ||||
|                     .add("add_manga[priority]", track.priority) | ||||
|                     .add("add_manga[storage_type]", track.storage_type) | ||||
|                     .add("add_manga[num_retail_volumes]", track.num_retail_volumes) | ||||
|                     .add("add_manga[num_read_times]", track.num_read_chapters) | ||||
|                     .add("add_manga[reread_value]", track.reread_value) | ||||
|                     .add("add_manga[comments]", track.comments) | ||||
|                     .add("add_manga[is_asked_to_discuss]", track.is_asked_to_discuss) | ||||
|                     .add("add_manga[sns_post_type]", track.sns_post_type) | ||||
|                     .add("submitIt", track.submitIt) | ||||
|                     .build() | ||||
|                 .add("entry_id", track.entry_id) | ||||
|                 .add("manga_id", track.manga_id) | ||||
|                 .add("add_manga[status]", track.status) | ||||
|                 .add("add_manga[num_read_volumes]", track.num_read_volumes) | ||||
|                 .add("last_completed_vol", track.last_completed_vol) | ||||
|                 .add("add_manga[num_read_chapters]", track.num_read_chapters) | ||||
|                 .add("add_manga[score]", track.score) | ||||
|                 .add("add_manga[start_date][month]", track.start_date_month) | ||||
|                 .add("add_manga[start_date][day]", track.start_date_day) | ||||
|                 .add("add_manga[start_date][year]", track.start_date_year) | ||||
|                 .add("add_manga[finish_date][month]", track.finish_date_month) | ||||
|                 .add("add_manga[finish_date][day]", track.finish_date_day) | ||||
|                 .add("add_manga[finish_date][year]", track.finish_date_year) | ||||
|                 .add("add_manga[tags]", track.tags) | ||||
|                 .add("add_manga[priority]", track.priority) | ||||
|                 .add("add_manga[storage_type]", track.storage_type) | ||||
|                 .add("add_manga[num_retail_volumes]", track.num_retail_volumes) | ||||
|                 .add("add_manga[num_read_times]", track.num_read_chapters) | ||||
|                 .add("add_manga[reread_value]", track.reread_value) | ||||
|                 .add("add_manga[comments]", track.comments) | ||||
|                 .add("add_manga[is_asked_to_discuss]", track.is_asked_to_discuss) | ||||
|                 .add("add_manga[sns_post_type]", track.sns_post_type) | ||||
|                 .add("submitIt", track.submitIt) | ||||
|                 .build() | ||||
|         } | ||||
|  | ||||
|         private fun Element.searchDateXml(field: String): Long { | ||||
|             val text = selectText(field, "0000-00-00")!! | ||||
|             // MAL sets the data to 0000-00-00 when date is invalid or missing | ||||
|             if (text == "0000-00-00") | ||||
|             if (text == "0000-00-00") { | ||||
|                 return 0L | ||||
|             } | ||||
|  | ||||
|             return SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(text)?.time ?: 0L | ||||
|         } | ||||
| @@ -359,8 +362,9 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI | ||||
|             val month = select(id + "_month > option[selected]").`val`().toIntOrNull() | ||||
|             val day = select(id + "_day > option[selected]").`val`().toIntOrNull() | ||||
|             val year = select(id + "_year > option[selected]").`val`().toIntOrNull() | ||||
|             if (year == null || month == null || day == null) | ||||
|             if (year == null || month == null || day == null) { | ||||
|                 return 0L | ||||
|             } | ||||
|  | ||||
|             return GregorianCalendar(year, month - 1, day).timeInMillis | ||||
|         } | ||||
| @@ -370,18 +374,18 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI | ||||
|         private fun Element.searchTotalChapters() = if (select(TD)[4].text() == "-") 0 else select(TD)[4].text().toInt() | ||||
|  | ||||
|         private fun Element.searchCoverUrl() = select("img") | ||||
|                 .attr("data-src") | ||||
|                 .split("\\?")[0] | ||||
|                 .replace("/r/50x70/", "/") | ||||
|             .attr("data-src") | ||||
|             .split("\\?")[0] | ||||
|             .replace("/r/50x70/", "/") | ||||
|  | ||||
|         private fun Element.searchMediaId() = select("div.picSurround") | ||||
|                 .select("a").attr("id") | ||||
|                 .replace("sarea", "") | ||||
|                 .toInt() | ||||
|             .select("a").attr("id") | ||||
|             .replace("sarea", "") | ||||
|             .toInt() | ||||
|  | ||||
|         private fun Element.searchSummary() = select("div.pt4") | ||||
|                 .first() | ||||
|                 .ownText()!! | ||||
|             .first() | ||||
|             .ownText()!! | ||||
|  | ||||
|         private fun Element.searchPublishingStatus() = if (select(TD).last().text() == "-") "Publishing" else "Finished" | ||||
|  | ||||
| @@ -472,8 +476,9 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI | ||||
|         fun copyPersonalFrom(track: Track) { | ||||
|             num_read_chapters = track.last_chapter_read.toString() | ||||
|             val numScore = track.score.toInt() | ||||
|             if (numScore in 1..9) | ||||
|             if (numScore in 1..9) { | ||||
|                 score = numScore.toString() | ||||
|             } | ||||
|             status = track.status.toString() | ||||
|             if (track.started_reading_date == 0L) { | ||||
|                 start_date_month = "" | ||||
|   | ||||
| @@ -53,7 +53,7 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor | ||||
|     private fun updateJsonBody(requestBody: RequestBody): RequestBody { | ||||
|         val jsonString = bodyToString(requestBody) | ||||
|         val newBody = JSONObject(jsonString) | ||||
|                 .put(MyAnimeListApi.CSRF, myanimelist.getCSRF()) | ||||
|             .put(MyAnimeListApi.CSRF, myanimelist.getCSRF()) | ||||
|  | ||||
|         return newBody.toString().toRequestBody(requestBody.contentType()) | ||||
|     } | ||||
|   | ||||
| @@ -51,18 +51,18 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) { | ||||
|  | ||||
|     override fun bind(track: Track): Observable<Track> { | ||||
|         return api.findLibManga(track, getUsername()) | ||||
|                 .flatMap { remoteTrack -> | ||||
|                     if (remoteTrack != null) { | ||||
|                         track.copyPersonalFrom(remoteTrack) | ||||
|                         track.library_id = remoteTrack.library_id | ||||
|                         update(track) | ||||
|                     } else { | ||||
|                         // Set default fields if it's not found in the list | ||||
|                         track.score = DEFAULT_SCORE.toFloat() | ||||
|                         track.status = DEFAULT_STATUS | ||||
|                         add(track) | ||||
|                     } | ||||
|             .flatMap { remoteTrack -> | ||||
|                 if (remoteTrack != null) { | ||||
|                     track.copyPersonalFrom(remoteTrack) | ||||
|                     track.library_id = remoteTrack.library_id | ||||
|                     update(track) | ||||
|                 } else { | ||||
|                     // Set default fields if it's not found in the list | ||||
|                     track.score = DEFAULT_SCORE.toFloat() | ||||
|                     track.status = DEFAULT_STATUS | ||||
|                     add(track) | ||||
|                 } | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     override fun search(query: String): Observable<List<TrackSearch>> { | ||||
| @@ -71,13 +71,13 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) { | ||||
|  | ||||
|     override fun refresh(track: Track): Observable<Track> { | ||||
|         return api.findLibManga(track, getUsername()) | ||||
|                 .map { remoteTrack -> | ||||
|                     if (remoteTrack != null) { | ||||
|                         track.copyPersonalFrom(remoteTrack) | ||||
|                         track.total_chapters = remoteTrack.total_chapters | ||||
|                     } | ||||
|                     track | ||||
|             .map { remoteTrack -> | ||||
|                 if (remoteTrack != null) { | ||||
|                     track.copyPersonalFrom(remoteTrack) | ||||
|                     track.total_chapters = remoteTrack.total_chapters | ||||
|                 } | ||||
|                 track | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     override fun getLogo() = R.drawable.ic_tracker_shikimori | ||||
|   | ||||
| @@ -30,49 +30,49 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter | ||||
|  | ||||
|     fun addLibManga(track: Track, user_id: String): Observable<Track> { | ||||
|         val payload = jsonObject( | ||||
|                 "user_rate" to jsonObject( | ||||
|                         "user_id" to user_id, | ||||
|                         "target_id" to track.media_id, | ||||
|                         "target_type" to "Manga", | ||||
|                         "chapters" to track.last_chapter_read, | ||||
|                         "score" to track.score.toInt(), | ||||
|                         "status" to track.toShikimoriStatus() | ||||
|                 ) | ||||
|             "user_rate" to jsonObject( | ||||
|                 "user_id" to user_id, | ||||
|                 "target_id" to track.media_id, | ||||
|                 "target_type" to "Manga", | ||||
|                 "chapters" to track.last_chapter_read, | ||||
|                 "score" to track.score.toInt(), | ||||
|                 "status" to track.toShikimoriStatus() | ||||
|             ) | ||||
|         ) | ||||
|         val body = payload.toString().toRequestBody(jsonime) | ||||
|         val request = Request.Builder() | ||||
|                 .url("$apiUrl/v2/user_rates") | ||||
|                 .post(body) | ||||
|                 .build() | ||||
|             .url("$apiUrl/v2/user_rates") | ||||
|             .post(body) | ||||
|             .build() | ||||
|         return authClient.newCall(request) | ||||
|                 .asObservableSuccess() | ||||
|                 .map { | ||||
|                     track | ||||
|                 } | ||||
|             .asObservableSuccess() | ||||
|             .map { | ||||
|                 track | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     fun updateLibManga(track: Track, user_id: String): Observable<Track> = addLibManga(track, user_id) | ||||
|  | ||||
|     fun search(search: String): Observable<List<TrackSearch>> { | ||||
|         val url = Uri.parse("$apiUrl/mangas").buildUpon() | ||||
|                 .appendQueryParameter("order", "popularity") | ||||
|                 .appendQueryParameter("search", search) | ||||
|                 .appendQueryParameter("limit", "20") | ||||
|                 .build() | ||||
|             .appendQueryParameter("order", "popularity") | ||||
|             .appendQueryParameter("search", search) | ||||
|             .appendQueryParameter("limit", "20") | ||||
|             .build() | ||||
|         val request = Request.Builder() | ||||
|                 .url(url.toString()) | ||||
|                 .get() | ||||
|                 .build() | ||||
|             .url(url.toString()) | ||||
|             .get() | ||||
|             .build() | ||||
|         return authClient.newCall(request) | ||||
|                 .asObservableSuccess() | ||||
|                 .map { netResponse -> | ||||
|                     val responseBody = netResponse.body?.string().orEmpty() | ||||
|                     if (responseBody.isEmpty()) { | ||||
|                         throw Exception("Null Response") | ||||
|                     } | ||||
|                     val response = JsonParser.parseString(responseBody).array | ||||
|                     response.map { jsonToSearch(it.obj) } | ||||
|             .asObservableSuccess() | ||||
|             .map { netResponse -> | ||||
|                 val responseBody = netResponse.body?.string().orEmpty() | ||||
|                 if (responseBody.isEmpty()) { | ||||
|                     throw Exception("Null Response") | ||||
|                 } | ||||
|                 val response = JsonParser.parseString(responseBody).array | ||||
|                 response.map { jsonToSearch(it.obj) } | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     private fun jsonToSearch(obj: JsonObject): TrackSearch { | ||||
| @@ -103,45 +103,45 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter | ||||
|  | ||||
|     fun findLibManga(track: Track, user_id: String): Observable<Track?> { | ||||
|         val url = Uri.parse("$apiUrl/v2/user_rates").buildUpon() | ||||
|                 .appendQueryParameter("user_id", user_id) | ||||
|                 .appendQueryParameter("target_id", track.media_id.toString()) | ||||
|                 .appendQueryParameter("target_type", "Manga") | ||||
|                 .build() | ||||
|             .appendQueryParameter("user_id", user_id) | ||||
|             .appendQueryParameter("target_id", track.media_id.toString()) | ||||
|             .appendQueryParameter("target_type", "Manga") | ||||
|             .build() | ||||
|         val request = Request.Builder() | ||||
|                 .url(url.toString()) | ||||
|                 .get() | ||||
|                 .build() | ||||
|             .url(url.toString()) | ||||
|             .get() | ||||
|             .build() | ||||
|  | ||||
|         val urlMangas = Uri.parse("$apiUrl/mangas").buildUpon() | ||||
|                 .appendPath(track.media_id.toString()) | ||||
|                 .build() | ||||
|             .appendPath(track.media_id.toString()) | ||||
|             .build() | ||||
|         val requestMangas = Request.Builder() | ||||
|                 .url(urlMangas.toString()) | ||||
|                 .get() | ||||
|                 .build() | ||||
|             .url(urlMangas.toString()) | ||||
|             .get() | ||||
|             .build() | ||||
|         return authClient.newCall(requestMangas) | ||||
|                 .asObservableSuccess() | ||||
|                 .map { netResponse -> | ||||
|                     val responseBody = netResponse.body?.string().orEmpty() | ||||
|                     JsonParser.parseString(responseBody).obj | ||||
|                 }.flatMap { mangas -> | ||||
|                     authClient.newCall(request) | ||||
|                             .asObservableSuccess() | ||||
|                             .map { netResponse -> | ||||
|                                 val responseBody = netResponse.body?.string().orEmpty() | ||||
|                                 if (responseBody.isEmpty()) { | ||||
|                                     throw Exception("Null Response") | ||||
|                                 } | ||||
|                                 val response = JsonParser.parseString(responseBody).array | ||||
|                                 if (response.size() > 1) { | ||||
|                                     throw Exception("Too much mangas in response") | ||||
|                                 } | ||||
|                                 val entry = response.map { | ||||
|                                     jsonToTrack(it.obj, mangas) | ||||
|                                 } | ||||
|                                 entry.firstOrNull() | ||||
|                             } | ||||
|                 } | ||||
|             .asObservableSuccess() | ||||
|             .map { netResponse -> | ||||
|                 val responseBody = netResponse.body?.string().orEmpty() | ||||
|                 JsonParser.parseString(responseBody).obj | ||||
|             }.flatMap { mangas -> | ||||
|                 authClient.newCall(request) | ||||
|                     .asObservableSuccess() | ||||
|                     .map { netResponse -> | ||||
|                         val responseBody = netResponse.body?.string().orEmpty() | ||||
|                         if (responseBody.isEmpty()) { | ||||
|                             throw Exception("Null Response") | ||||
|                         } | ||||
|                         val response = JsonParser.parseString(responseBody).array | ||||
|                         if (response.size() > 1) { | ||||
|                             throw Exception("Too much mangas in response") | ||||
|                         } | ||||
|                         val entry = response.map { | ||||
|                             jsonToTrack(it.obj, mangas) | ||||
|                         } | ||||
|                         entry.firstOrNull() | ||||
|                     } | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     fun getCurrentUser(): Int { | ||||
| @@ -159,14 +159,15 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun accessTokenRequest(code: String) = POST(oauthUrl, | ||||
|             body = FormBody.Builder() | ||||
|                     .add("grant_type", "authorization_code") | ||||
|                     .add("client_id", clientId) | ||||
|                     .add("client_secret", clientSecret) | ||||
|                     .add("code", code) | ||||
|                     .add("redirect_uri", redirectUrl) | ||||
|                     .build() | ||||
|     private fun accessTokenRequest(code: String) = POST( | ||||
|         oauthUrl, | ||||
|         body = FormBody.Builder() | ||||
|             .add("grant_type", "authorization_code") | ||||
|             .add("client_id", clientId) | ||||
|             .add("client_secret", clientSecret) | ||||
|             .add("code", code) | ||||
|             .add("redirect_uri", redirectUrl) | ||||
|             .build() | ||||
|     ) | ||||
|  | ||||
|     companion object { | ||||
| @@ -186,18 +187,20 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter | ||||
|         } | ||||
|  | ||||
|         fun authUrl() = | ||||
|                 Uri.parse(loginUrl).buildUpon() | ||||
|                         .appendQueryParameter("client_id", clientId) | ||||
|                         .appendQueryParameter("redirect_uri", redirectUrl) | ||||
|                         .appendQueryParameter("response_type", "code") | ||||
|                         .build() | ||||
|             Uri.parse(loginUrl).buildUpon() | ||||
|                 .appendQueryParameter("client_id", clientId) | ||||
|                 .appendQueryParameter("redirect_uri", redirectUrl) | ||||
|                 .appendQueryParameter("response_type", "code") | ||||
|                 .build() | ||||
|  | ||||
|         fun refreshTokenRequest(token: String) = POST(oauthUrl, | ||||
|                 body = FormBody.Builder() | ||||
|                         .add("grant_type", "refresh_token") | ||||
|                         .add("client_id", clientId) | ||||
|                         .add("client_secret", clientSecret) | ||||
|                         .add("refresh_token", token) | ||||
|                         .build()) | ||||
|         fun refreshTokenRequest(token: String) = POST( | ||||
|             oauthUrl, | ||||
|             body = FormBody.Builder() | ||||
|                 .add("grant_type", "refresh_token") | ||||
|                 .add("client_id", clientId) | ||||
|                 .add("client_secret", clientSecret) | ||||
|                 .add("refresh_token", token) | ||||
|                 .build() | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -29,9 +29,9 @@ class ShikimoriInterceptor(val shikimori: Shikimori, val gson: Gson) : Intercept | ||||
|         } | ||||
|         // Add the authorization header to the original request. | ||||
|         val authRequest = originalRequest.newBuilder() | ||||
|                 .addHeader("Authorization", "Bearer ${oauth!!.access_token}") | ||||
|                 .header("User-Agent", "Tachiyomi") | ||||
|                 .build() | ||||
|             .addHeader("Authorization", "Bearer ${oauth!!.access_token}") | ||||
|             .header("User-Agent", "Tachiyomi") | ||||
|             .build() | ||||
|  | ||||
|         return chain.proceed(authRequest) | ||||
|     } | ||||
|   | ||||
| @@ -18,7 +18,7 @@ import java.util.concurrent.TimeUnit | ||||
| import kotlinx.coroutines.runBlocking | ||||
|  | ||||
| class UpdaterJob(private val context: Context, workerParams: WorkerParameters) : | ||||
|         Worker(context, workerParams) { | ||||
|     Worker(context, workerParams) { | ||||
|  | ||||
|     override fun doWork(): Result { | ||||
|         return runBlocking { | ||||
| @@ -37,9 +37,11 @@ class UpdaterJob(private val context: Context, workerParams: WorkerParameters) : | ||||
|                         setContentText(context.getString(R.string.update_check_notification_update_available)) | ||||
|                         setSmallIcon(android.R.drawable.stat_sys_download_done) | ||||
|                         // Download action | ||||
|                         addAction(android.R.drawable.stat_sys_download_done, | ||||
|                                 context.getString(R.string.action_download), | ||||
|                                 PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)) | ||||
|                         addAction( | ||||
|                             android.R.drawable.stat_sys_download_done, | ||||
|                             context.getString(R.string.action_download), | ||||
|                             PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) | ||||
|                         ) | ||||
|                     } | ||||
|                 } | ||||
|                 Result.success() | ||||
| @@ -59,15 +61,16 @@ class UpdaterJob(private val context: Context, workerParams: WorkerParameters) : | ||||
|  | ||||
|         fun setupTask(context: Context) { | ||||
|             val constraints = Constraints.Builder() | ||||
|                     .setRequiredNetworkType(NetworkType.CONNECTED) | ||||
|                     .build() | ||||
|                 .setRequiredNetworkType(NetworkType.CONNECTED) | ||||
|                 .build() | ||||
|  | ||||
|             val request = PeriodicWorkRequestBuilder<UpdaterJob>( | ||||
|                     3, TimeUnit.DAYS, | ||||
|                     3, TimeUnit.HOURS) | ||||
|                     .addTag(TAG) | ||||
|                     .setConstraints(constraints) | ||||
|                     .build() | ||||
|                 3, TimeUnit.DAYS, | ||||
|                 3, TimeUnit.HOURS | ||||
|             ) | ||||
|                 .addTag(TAG) | ||||
|                 .setConstraints(constraints) | ||||
|                 .build() | ||||
|  | ||||
|             WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request) | ||||
|         } | ||||
|   | ||||
| @@ -69,13 +69,17 @@ internal class UpdaterNotifier(private val context: Context) { | ||||
|             setProgress(0, 0, false) | ||||
|             // Install action | ||||
|             setContentIntent(NotificationHandler.installApkPendingActivity(context, uri)) | ||||
|             addAction(R.drawable.ic_system_update_alt_white_24dp, | ||||
|                     context.getString(R.string.action_install), | ||||
|                     NotificationHandler.installApkPendingActivity(context, uri)) | ||||
|             addAction( | ||||
|                 R.drawable.ic_system_update_alt_white_24dp, | ||||
|                 context.getString(R.string.action_install), | ||||
|                 NotificationHandler.installApkPendingActivity(context, uri) | ||||
|             ) | ||||
|             // Cancel action | ||||
|             addAction(R.drawable.ic_close_24dp, | ||||
|                     context.getString(R.string.action_cancel), | ||||
|                     NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER)) | ||||
|             addAction( | ||||
|                 R.drawable.ic_close_24dp, | ||||
|                 context.getString(R.string.action_cancel), | ||||
|                 NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER) | ||||
|             ) | ||||
|         } | ||||
|         notificationBuilder.show() | ||||
|     } | ||||
| @@ -92,13 +96,17 @@ internal class UpdaterNotifier(private val context: Context) { | ||||
|             setOnlyAlertOnce(false) | ||||
|             setProgress(0, 0, false) | ||||
|             // Retry action | ||||
|             addAction(R.drawable.ic_refresh_24dp, | ||||
|                     context.getString(R.string.action_retry), | ||||
|                     UpdaterService.downloadApkPendingService(context, url)) | ||||
|             addAction( | ||||
|                 R.drawable.ic_refresh_24dp, | ||||
|                 context.getString(R.string.action_retry), | ||||
|                 UpdaterService.downloadApkPendingService(context, url) | ||||
|             ) | ||||
|             // Cancel action | ||||
|             addAction(R.drawable.ic_close_24dp, | ||||
|                     context.getString(R.string.action_cancel), | ||||
|                     NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER)) | ||||
|             addAction( | ||||
|                 R.drawable.ic_close_24dp, | ||||
|                 context.getString(R.string.action_cancel), | ||||
|                 NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER) | ||||
|             ) | ||||
|         } | ||||
|         notificationBuilder.show(Notifications.ID_UPDATER) | ||||
|     } | ||||
|   | ||||
| @@ -16,8 +16,8 @@ class DevRepoUpdateChecker : UpdateChecker() { | ||||
|  | ||||
|     private val client: OkHttpClient by lazy { | ||||
|         Injekt.get<NetworkHelper>().client.newBuilder() | ||||
|                 .followRedirects(false) | ||||
|                 .build() | ||||
|             .followRedirects(false) | ||||
|             .build() | ||||
|     } | ||||
|  | ||||
|     private val versionRegex: Regex by lazy { | ||||
|   | ||||
| @@ -15,10 +15,10 @@ interface GithubService { | ||||
|     companion object { | ||||
|         fun create(): GithubService { | ||||
|             val restAdapter = Retrofit.Builder() | ||||
|                     .baseUrl("https://api.github.com") | ||||
|                     .addConverterFactory(GsonConverterFactory.create()) | ||||
|                     .client(Injekt.get<NetworkHelper>().client) | ||||
|                     .build() | ||||
|                 .baseUrl("https://api.github.com") | ||||
|                 .addConverterFactory(GsonConverterFactory.create()) | ||||
|                 .client(Injekt.get<NetworkHelper>().client) | ||||
|                 .build() | ||||
|  | ||||
|             return restAdapter.create(GithubService::class.java) | ||||
|         } | ||||
|   | ||||
| @@ -119,16 +119,16 @@ class ExtensionManager( | ||||
|         val extensions = ExtensionLoader.loadExtensions(context) | ||||
|  | ||||
|         installedExtensions = extensions | ||||
|                 .filterIsInstance<LoadResult.Success>() | ||||
|                 .map { it.extension } | ||||
|             .filterIsInstance<LoadResult.Success>() | ||||
|             .map { it.extension } | ||||
|         installedExtensions | ||||
|                 .flatMap { it.sources } | ||||
|                 // overwrite is needed until the bundled sources are removed | ||||
|                 .forEach { sourceManager.registerSource(it, true) } | ||||
|             .flatMap { it.sources } | ||||
|             // overwrite is needed until the bundled sources are removed | ||||
|             .forEach { sourceManager.registerSource(it, true) } | ||||
|  | ||||
|         untrustedExtensions = extensions | ||||
|                 .filterIsInstance<LoadResult.Untrusted>() | ||||
|                 .map { it.extension } | ||||
|             .filterIsInstance<LoadResult.Untrusted>() | ||||
|             .map { it.extension } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -223,7 +223,7 @@ class ExtensionManager( | ||||
|      */ | ||||
|     fun updateExtension(extension: Extension.Installed): Observable<InstallStep> { | ||||
|         val availableExt = availableExtensions.find { it.pkgName == extension.pkgName } | ||||
|                 ?: return Observable.empty() | ||||
|             ?: return Observable.empty() | ||||
|         return installExtension(availableExt) | ||||
|     } | ||||
|  | ||||
| @@ -266,15 +266,15 @@ class ExtensionManager( | ||||
|         val ctx = context | ||||
|         launchNow { | ||||
|             nowTrustedExtensions | ||||
|                     .map { extension -> | ||||
|                         async { ExtensionLoader.loadExtensionFromPkgName(ctx, extension.pkgName) } | ||||
|                     } | ||||
|                     .map { it.await() } | ||||
|                     .forEach { result -> | ||||
|                         if (result is LoadResult.Success) { | ||||
|                             registerNewExtension(result.extension) | ||||
|                         } | ||||
|                 .map { extension -> | ||||
|                     async { ExtensionLoader.loadExtensionFromPkgName(ctx, extension.pkgName) } | ||||
|                 } | ||||
|                 .map { it.await() } | ||||
|                 .forEach { result -> | ||||
|                     if (result is LoadResult.Success) { | ||||
|                         registerNewExtension(result.extension) | ||||
|                     } | ||||
|                 } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -40,7 +40,8 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam | ||||
|  | ||||
|     private fun createUpdateNotification(names: List<String>) { | ||||
|         NotificationManagerCompat.from(context).apply { | ||||
|             notify(Notifications.ID_UPDATES_TO_EXTS, | ||||
|             notify( | ||||
|                 Notifications.ID_UPDATES_TO_EXTS, | ||||
|                 context.notification(Notifications.CHANNEL_UPDATES_TO_EXTS) { | ||||
|                     setContentTitle( | ||||
|                         context.resources.getQuantityString( | ||||
| @@ -55,7 +56,8 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam | ||||
|                     setSmallIcon(R.drawable.ic_extension_24dp) | ||||
|                     setContentIntent(NotificationReceiver.openExtensionsPendingActivity(context)) | ||||
|                     setAutoCancel(true) | ||||
|                 }) | ||||
|                 } | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -72,7 +74,8 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam | ||||
|  | ||||
|                 val request = PeriodicWorkRequestBuilder<ExtensionUpdateJob>( | ||||
|                     12, TimeUnit.HOURS, | ||||
|                     1, TimeUnit.HOURS) | ||||
|                     1, TimeUnit.HOURS | ||||
|                 ) | ||||
|                     .addTag(TAG) | ||||
|                     .setConstraints(constraints) | ||||
|                     .build() | ||||
|   | ||||
| @@ -70,22 +70,22 @@ internal class ExtensionGithubApi { | ||||
|         val json = gson.fromJson<JsonArray>(text) | ||||
|  | ||||
|         return json | ||||
|                 .filter { element -> | ||||
|                     val versionName = element["version"].string | ||||
|                     val libVersion = versionName.substringBeforeLast('.').toDouble() | ||||
|                     libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX | ||||
|                 } | ||||
|                 .map { element -> | ||||
|                     val name = element["name"].string.substringAfter("Tachiyomi: ") | ||||
|                     val pkgName = element["pkg"].string | ||||
|                     val apkName = element["apk"].string | ||||
|                     val versionName = element["version"].string | ||||
|                     val versionCode = element["code"].int | ||||
|                     val lang = element["lang"].string | ||||
|                     val icon = "$REPO_URL/icon/${apkName.replace(".apk", ".png")}" | ||||
|             .filter { element -> | ||||
|                 val versionName = element["version"].string | ||||
|                 val libVersion = versionName.substringBeforeLast('.').toDouble() | ||||
|                 libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX | ||||
|             } | ||||
|             .map { element -> | ||||
|                 val name = element["name"].string.substringAfter("Tachiyomi: ") | ||||
|                 val pkgName = element["pkg"].string | ||||
|                 val apkName = element["apk"].string | ||||
|                 val versionName = element["version"].string | ||||
|                 val versionCode = element["code"].int | ||||
|                 val lang = element["lang"].string | ||||
|                 val icon = "$REPO_URL/icon/${apkName.replace(".apk", ".png")}" | ||||
|  | ||||
|                     Extension.Available(name, pkgName, versionName, versionCode, lang, apkName, icon) | ||||
|                 } | ||||
|                 Extension.Available(name, pkgName, versionName, versionCode, lang, apkName, icon) | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     fun getApkUrl(extension: Extension.Available): String { | ||||
|   | ||||
| @@ -18,9 +18,9 @@ class ExtensionInstallActivity : Activity() { | ||||
|         super.onCreate(savedInstanceState) | ||||
|  | ||||
|         val installIntent = Intent(Intent.ACTION_INSTALL_PACKAGE) | ||||
|                 .setDataAndType(intent.data, intent.type) | ||||
|                 .putExtra(Intent.EXTRA_RETURN_RESULT, true) | ||||
|                 .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) | ||||
|             .setDataAndType(intent.data, intent.type) | ||||
|             .putExtra(Intent.EXTRA_RETURN_RESULT, true) | ||||
|             .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) | ||||
|  | ||||
|         try { | ||||
|             startActivityForResult(installIntent, INSTALL_REQUEST_CODE) | ||||
|   | ||||
| @@ -19,7 +19,7 @@ import kotlinx.coroutines.async | ||||
|  * @param listener The listener that should be notified of extension installation events. | ||||
|  */ | ||||
| internal class ExtensionInstallReceiver(private val listener: Listener) : | ||||
|         BroadcastReceiver() { | ||||
|     BroadcastReceiver() { | ||||
|  | ||||
|     /** | ||||
|      * Registers this broadcast receiver | ||||
| @@ -93,7 +93,7 @@ internal class ExtensionInstallReceiver(private val listener: Listener) : | ||||
|      */ | ||||
|     private suspend fun getExtensionFromIntent(context: Context, intent: Intent?): LoadResult { | ||||
|         val pkgName = getPackageNameFromIntent(intent) | ||||
|                 ?: return LoadResult.Error("Package name not found") | ||||
|             ?: return LoadResult.Error("Package name not found") | ||||
|         return GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT) { ExtensionLoader.loadExtensionFromPkgName(context, pkgName) }.await() | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -65,26 +65,26 @@ internal class ExtensionInstaller(private val context: Context) { | ||||
|  | ||||
|         val downloadUri = Uri.parse(url) | ||||
|         val request = DownloadManager.Request(downloadUri) | ||||
|                 .setTitle(extension.name) | ||||
|                 .setMimeType(APK_MIME) | ||||
|                 .setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, downloadUri.lastPathSegment) | ||||
|                 .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) | ||||
|             .setTitle(extension.name) | ||||
|             .setMimeType(APK_MIME) | ||||
|             .setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, downloadUri.lastPathSegment) | ||||
|             .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) | ||||
|  | ||||
|         val id = downloadManager.enqueue(request) | ||||
|         activeDownloads[pkgName] = id | ||||
|  | ||||
|         downloadsRelay.filter { it.first == id } | ||||
|                 .map { it.second } | ||||
|                 // Poll download status | ||||
|                 .mergeWith(pollStatus(id)) | ||||
|                 // Force an error if the download takes more than 3 minutes | ||||
|                 .mergeWith(Observable.timer(3, TimeUnit.MINUTES).map { InstallStep.Error }) | ||||
|                 // Stop when the application is installed or errors | ||||
|                 .takeUntil { it.isCompleted() } | ||||
|                 // Always notify on main thread | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 // Always remove the download when unsubscribed | ||||
|                 .doOnUnsubscribe { deleteDownload(pkgName) } | ||||
|             .map { it.second } | ||||
|             // Poll download status | ||||
|             .mergeWith(pollStatus(id)) | ||||
|             // Force an error if the download takes more than 3 minutes | ||||
|             .mergeWith(Observable.timer(3, TimeUnit.MINUTES).map { InstallStep.Error }) | ||||
|             // Stop when the application is installed or errors | ||||
|             .takeUntil { it.isCompleted() } | ||||
|             // Always notify on main thread | ||||
|             .observeOn(AndroidSchedulers.mainThread()) | ||||
|             // Always remove the download when unsubscribed | ||||
|             .doOnUnsubscribe { deleteDownload(pkgName) } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -97,25 +97,25 @@ internal class ExtensionInstaller(private val context: Context) { | ||||
|         val query = DownloadManager.Query().setFilterById(id) | ||||
|  | ||||
|         return Observable.interval(0, 1, TimeUnit.SECONDS) | ||||
|                 // Get the current download status | ||||
|                 .map { | ||||
|                     downloadManager.query(query).use { cursor -> | ||||
|                         cursor.moveToFirst() | ||||
|                         cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)) | ||||
|                     } | ||||
|             // Get the current download status | ||||
|             .map { | ||||
|                 downloadManager.query(query).use { cursor -> | ||||
|                     cursor.moveToFirst() | ||||
|                     cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)) | ||||
|                 } | ||||
|                 // Ignore duplicate results | ||||
|                 .distinctUntilChanged() | ||||
|                 // Stop polling when the download fails or finishes | ||||
|                 .takeUntil { it == DownloadManager.STATUS_SUCCESSFUL || it == DownloadManager.STATUS_FAILED } | ||||
|                 // Map to our model | ||||
|                 .flatMap { status -> | ||||
|                     when (status) { | ||||
|                         DownloadManager.STATUS_PENDING -> Observable.just(InstallStep.Pending) | ||||
|                         DownloadManager.STATUS_RUNNING -> Observable.just(InstallStep.Downloading) | ||||
|                         else -> Observable.empty() | ||||
|                     } | ||||
|             } | ||||
|             // Ignore duplicate results | ||||
|             .distinctUntilChanged() | ||||
|             // Stop polling when the download fails or finishes | ||||
|             .takeUntil { it == DownloadManager.STATUS_SUCCESSFUL || it == DownloadManager.STATUS_FAILED } | ||||
|             // Map to our model | ||||
|             .flatMap { status -> | ||||
|                 when (status) { | ||||
|                     DownloadManager.STATUS_PENDING -> Observable.just(InstallStep.Pending) | ||||
|                     DownloadManager.STATUS_RUNNING -> Observable.just(InstallStep.Downloading) | ||||
|                     else -> Observable.empty() | ||||
|                 } | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -125,9 +125,9 @@ internal class ExtensionInstaller(private val context: Context) { | ||||
|      */ | ||||
|     fun installApk(downloadId: Long, uri: Uri) { | ||||
|         val intent = Intent(context, ExtensionInstallActivity::class.java) | ||||
|                 .setDataAndType(uri, APK_MIME) | ||||
|                 .putExtra(EXTRA_DOWNLOAD_ID, downloadId) | ||||
|                 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION) | ||||
|             .setDataAndType(uri, APK_MIME) | ||||
|             .putExtra(EXTRA_DOWNLOAD_ID, downloadId) | ||||
|             .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION) | ||||
|  | ||||
|         context.startActivity(intent) | ||||
|     } | ||||
| @@ -140,7 +140,7 @@ internal class ExtensionInstaller(private val context: Context) { | ||||
|     fun uninstallApk(pkgName: String) { | ||||
|         val packageUri = Uri.parse("package:$pkgName") | ||||
|         val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri) | ||||
|                 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) | ||||
|             .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) | ||||
|  | ||||
|         context.startActivity(intent) | ||||
|     } | ||||
| @@ -227,7 +227,7 @@ internal class ExtensionInstaller(private val context: Context) { | ||||
|             downloadManager.query(query).use { cursor -> | ||||
|                 if (cursor.moveToFirst()) { | ||||
|                     val localUri = cursor.getString( | ||||
|                             cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI) | ||||
|                         cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI) | ||||
|                     ).removePrefix(FILE_SCHEME) | ||||
|  | ||||
|                     installApk(id, File(localUri).getUriCompat(context)) | ||||
|   | ||||
| @@ -35,9 +35,9 @@ internal object ExtensionLoader { | ||||
|      * List of the trusted signatures. | ||||
|      */ | ||||
|     var trustedSignatures = mutableSetOf<String>() + | ||||
|             Injekt.get<PreferencesHelper>().trustedSignatures().get() + | ||||
|             // inorichi's key | ||||
|             "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23" | ||||
|         Injekt.get<PreferencesHelper>().trustedSignatures().get() + | ||||
|         // inorichi's key | ||||
|         "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23" | ||||
|  | ||||
|     /** | ||||
|      * Return a list of all the installed extensions initialized concurrently. | ||||
| @@ -107,8 +107,10 @@ internal object ExtensionLoader { | ||||
|         // Validate lib version | ||||
|         val libVersion = versionName.substringBeforeLast('.').toDouble() | ||||
|         if (libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) { | ||||
|             val exception = Exception("Lib version is $libVersion, while only versions " + | ||||
|                     "$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed") | ||||
|             val exception = Exception( | ||||
|                 "Lib version is $libVersion, while only versions " + | ||||
|                     "$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed" | ||||
|             ) | ||||
|             Timber.w(exception) | ||||
|             return LoadResult.Error(exception) | ||||
|         } | ||||
| @@ -126,29 +128,30 @@ internal object ExtensionLoader { | ||||
|         val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader) | ||||
|  | ||||
|         val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!! | ||||
|                 .split(";") | ||||
|                 .map { | ||||
|                     val sourceClass = it.trim() | ||||
|                     if (sourceClass.startsWith(".")) | ||||
|                         pkgInfo.packageName + sourceClass | ||||
|                     else | ||||
|                         sourceClass | ||||
|             .split(";") | ||||
|             .map { | ||||
|                 val sourceClass = it.trim() | ||||
|                 if (sourceClass.startsWith(".")) { | ||||
|                     pkgInfo.packageName + sourceClass | ||||
|                 } else { | ||||
|                     sourceClass | ||||
|                 } | ||||
|                 .flatMap { | ||||
|                     try { | ||||
|                         when (val obj = Class.forName(it, false, classLoader).newInstance()) { | ||||
|                             is Source -> listOf(obj) | ||||
|                             is SourceFactory -> obj.createSources() | ||||
|                             else -> throw Exception("Unknown source class type! ${obj.javaClass}") | ||||
|                         } | ||||
|                     } catch (e: Throwable) { | ||||
|                         Timber.e(e, "Extension load error: $extName.") | ||||
|                         return LoadResult.Error(e) | ||||
|             } | ||||
|             .flatMap { | ||||
|                 try { | ||||
|                     when (val obj = Class.forName(it, false, classLoader).newInstance()) { | ||||
|                         is Source -> listOf(obj) | ||||
|                         is SourceFactory -> obj.createSources() | ||||
|                         else -> throw Exception("Unknown source class type! ${obj.javaClass}") | ||||
|                     } | ||||
|                 } catch (e: Throwable) { | ||||
|                     Timber.e(e, "Extension load error: $extName.") | ||||
|                     return LoadResult.Error(e) | ||||
|                 } | ||||
|             } | ||||
|         val langs = sources.filterIsInstance<CatalogueSource>() | ||||
|                 .map { it.lang } | ||||
|                 .toSet() | ||||
|             .map { it.lang } | ||||
|             .toSet() | ||||
|  | ||||
|         val lang = when (langs.size) { | ||||
|             0 -> "" | ||||
|   | ||||
| @@ -44,9 +44,9 @@ class AndroidCookieJar : CookieJar { | ||||
|         } | ||||
|  | ||||
|         cookies.split(";") | ||||
|                 .map { it.substringBefore("=") } | ||||
|                 .filterNames() | ||||
|                 .onEach { manager.setCookie(urlString, "$it=;Max-Age=$maxAge") } | ||||
|             .map { it.substringBefore("=") } | ||||
|             .filterNames() | ||||
|             .onEach { manager.setCookie(urlString, "$it=;Max-Age=$maxAge") } | ||||
|     } | ||||
|  | ||||
|     fun removeAll() { | ||||
|   | ||||
| @@ -54,7 +54,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor { | ||||
|             response.close() | ||||
|             networkHelper.cookieManager.remove(originalRequest.url, COOKIE_NAMES, 0) | ||||
|             val oldCookie = networkHelper.cookieManager.get(originalRequest.url) | ||||
|                     .firstOrNull { it.name == "cf_clearance" } | ||||
|                 .firstOrNull { it.name == "cf_clearance" } | ||||
|             resolveWithWebView(originalRequest, oldCookie) | ||||
|  | ||||
|             return chain.proceed(originalRequest) | ||||
| @@ -87,14 +87,14 @@ class CloudflareInterceptor(private val context: Context) : Interceptor { | ||||
|  | ||||
|             // Avoid set empty User-Agent, Chromium WebView will reset to default if empty | ||||
|             webview.settings.userAgentString = request.header("User-Agent") | ||||
|                     ?: HttpSource.DEFAULT_USERAGENT | ||||
|                 ?: HttpSource.DEFAULT_USERAGENT | ||||
|  | ||||
|             webview.webViewClient = object : WebViewClientCompat() { | ||||
|                 override fun onPageFinished(view: WebView, url: String) { | ||||
|                     fun isCloudFlareBypassed(): Boolean { | ||||
|                         return networkHelper.cookieManager.get(origRequestUrl.toHttpUrl()) | ||||
|                                 .firstOrNull { it.name == "cf_clearance" } | ||||
|                                 .let { it != null && it != oldCookie } | ||||
|                             .firstOrNull { it.name == "cf_clearance" } | ||||
|                             .let { it != null && it != oldCookie } | ||||
|                     } | ||||
|  | ||||
|                     if (isCloudFlareBypassed()) { | ||||
|   | ||||
| @@ -14,12 +14,12 @@ class NetworkHelper(context: Context) { | ||||
|     val cookieManager = AndroidCookieJar() | ||||
|  | ||||
|     val client = OkHttpClient.Builder() | ||||
|             .cookieJar(cookieManager) | ||||
|             .cache(Cache(cacheDir, cacheSize)) | ||||
|             .build() | ||||
|         .cookieJar(cookieManager) | ||||
|         .cache(Cache(cacheDir, cacheSize)) | ||||
|         .build() | ||||
|  | ||||
|     val cloudflareClient = client.newBuilder() | ||||
|             .addInterceptor(UserAgentInterceptor()) | ||||
|             .addInterceptor(CloudflareInterceptor(context)) | ||||
|             .build() | ||||
|         .addInterceptor(UserAgentInterceptor()) | ||||
|         .addInterceptor(CloudflareInterceptor(context)) | ||||
|         .build() | ||||
| } | ||||
|   | ||||
| @@ -92,14 +92,14 @@ fun Call.asObservableSuccess(): Observable<Response> { | ||||
|  | ||||
| fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call { | ||||
|     val progressClient = newBuilder() | ||||
|             .cache(null) | ||||
|             .addNetworkInterceptor { chain -> | ||||
|                 val originalResponse = chain.proceed(chain.request()) | ||||
|                 originalResponse.newBuilder() | ||||
|                         .body(ProgressResponseBody(originalResponse.body!!, listener)) | ||||
|                         .build() | ||||
|             } | ||||
|             .build() | ||||
|         .cache(null) | ||||
|         .addNetworkInterceptor { chain -> | ||||
|             val originalResponse = chain.proceed(chain.request()) | ||||
|             originalResponse.newBuilder() | ||||
|                 .body(ProgressResponseBody(originalResponse.body!!, listener)) | ||||
|                 .build() | ||||
|         } | ||||
|         .build() | ||||
|  | ||||
|     return progressClient.newCall(request) | ||||
| } | ||||
|   | ||||
| @@ -17,10 +17,10 @@ fun GET( | ||||
|     cache: CacheControl = DEFAULT_CACHE_CONTROL | ||||
| ): Request { | ||||
|     return Request.Builder() | ||||
|             .url(url) | ||||
|             .headers(headers) | ||||
|             .cacheControl(cache) | ||||
|             .build() | ||||
|         .url(url) | ||||
|         .headers(headers) | ||||
|         .cacheControl(cache) | ||||
|         .build() | ||||
| } | ||||
|  | ||||
| fun POST( | ||||
| @@ -30,9 +30,9 @@ fun POST( | ||||
|     cache: CacheControl = DEFAULT_CACHE_CONTROL | ||||
| ): Request { | ||||
|     return Request.Builder() | ||||
|             .url(url) | ||||
|             .post(body) | ||||
|             .headers(headers) | ||||
|             .cacheControl(cache) | ||||
|             .build() | ||||
|         .url(url) | ||||
|         .post(body) | ||||
|         .headers(headers) | ||||
|         .cacheControl(cache) | ||||
|         .build() | ||||
| } | ||||
|   | ||||
| @@ -10,10 +10,10 @@ class UserAgentInterceptor : Interceptor { | ||||
|  | ||||
|         return if (originalRequest.header("User-Agent").isNullOrEmpty()) { | ||||
|             val newRequest = originalRequest | ||||
|                     .newBuilder() | ||||
|                     .removeHeader("User-Agent") | ||||
|                     .addHeader("User-Agent", HttpSource.DEFAULT_USERAGENT) | ||||
|                     .build() | ||||
|                 .newBuilder() | ||||
|                 .removeHeader("User-Agent") | ||||
|                 .addHeader("User-Agent", HttpSource.DEFAULT_USERAGENT) | ||||
|                 .build() | ||||
|             chain.proceed(newRequest) | ||||
|         } else { | ||||
|             chain.proceed(originalRequest) | ||||
|   | ||||
| @@ -76,23 +76,25 @@ class LocalSource(private val context: Context) : CatalogueSource { | ||||
|  | ||||
|         val time = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L | ||||
|         var mangaDirs = baseDirs.mapNotNull { it.listFiles()?.toList() } | ||||
|                 .flatten() | ||||
|                 .filter { it.isDirectory && if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time } | ||||
|                 .distinctBy { it.name } | ||||
|             .flatten() | ||||
|             .filter { it.isDirectory && if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time } | ||||
|             .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 = if (state.ascending) { | ||||
|                     mangaDirs.sortedBy { it.name.toLowerCase(Locale.ENGLISH) } | ||||
|                 else | ||||
|                 } else { | ||||
|                     mangaDirs.sortedByDescending { it.name.toLowerCase(Locale.ENGLISH) } | ||||
|                 } | ||||
|             } | ||||
|             1 -> { | ||||
|                 mangaDirs = if (state.ascending) | ||||
|                 mangaDirs = if (state.ascending) { | ||||
|                     mangaDirs.sortedBy(File::lastModified) | ||||
|                 else | ||||
|                 } else { | ||||
|                     mangaDirs.sortedByDescending(File::lastModified) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @@ -131,47 +133,49 @@ class LocalSource(private val context: Context) : CatalogueSource { | ||||
|  | ||||
|     override fun fetchMangaDetails(manga: SManga): Observable<SManga> { | ||||
|         getBaseDirectories(context) | ||||
|                 .mapNotNull { File(it, manga.url).listFiles()?.toList() } | ||||
|                 .flatten() | ||||
|                 .firstOrNull { it.extension == "json" } | ||||
|                 ?.apply { | ||||
|                     val json = Gson().fromJson(Scanner(this).useDelimiter("\\Z").next(), JsonObject::class.java) | ||||
|                     manga.title = json["title"]?.asString ?: manga.title | ||||
|                     manga.author = json["author"]?.asString ?: manga.author | ||||
|                     manga.artist = json["artist"]?.asString ?: manga.artist | ||||
|                     manga.description = json["description"]?.asString ?: manga.description | ||||
|                     manga.genre = json["genre"]?.asJsonArray?.joinToString(", ") { it.asString } | ||||
|                             ?: manga.genre | ||||
|                     manga.status = json["status"]?.asInt ?: manga.status | ||||
|                 } | ||||
|             .mapNotNull { File(it, manga.url).listFiles()?.toList() } | ||||
|             .flatten() | ||||
|             .firstOrNull { it.extension == "json" } | ||||
|             ?.apply { | ||||
|                 val json = Gson().fromJson(Scanner(this).useDelimiter("\\Z").next(), JsonObject::class.java) | ||||
|                 manga.title = json["title"]?.asString ?: manga.title | ||||
|                 manga.author = json["author"]?.asString ?: manga.author | ||||
|                 manga.artist = json["artist"]?.asString ?: manga.artist | ||||
|                 manga.description = json["description"]?.asString ?: manga.description | ||||
|                 manga.genre = json["genre"]?.asJsonArray?.joinToString(", ") { it.asString } | ||||
|                     ?: manga.genre | ||||
|                 manga.status = json["status"]?.asInt ?: manga.status | ||||
|             } | ||||
|         return Observable.just(manga) | ||||
|     } | ||||
|  | ||||
|     override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> { | ||||
|         val chapters = getBaseDirectories(context) | ||||
|                 .asSequence() | ||||
|                 .mapNotNull { File(it, manga.url).listFiles()?.toList() } | ||||
|                 .flatten() | ||||
|                 .filter { it.isDirectory || isSupportedFile(it.extension) } | ||||
|                 .map { chapterFile -> | ||||
|                     SChapter.create().apply { | ||||
|                         url = "${manga.url}/${chapterFile.name}" | ||||
|                         val chapName = if (chapterFile.isDirectory) { | ||||
|                             chapterFile.name | ||||
|                         } else { | ||||
|                             chapterFile.nameWithoutExtension | ||||
|                         } | ||||
|                         val chapNameCut = chapName.replace(manga.title, "", true).trim(' ', '-', '_') | ||||
|                         name = if (chapNameCut.isEmpty()) chapName else chapNameCut | ||||
|                         date_upload = chapterFile.lastModified() | ||||
|                         ChapterRecognition.parseChapterNumber(this, manga) | ||||
|             .asSequence() | ||||
|             .mapNotNull { File(it, manga.url).listFiles()?.toList() } | ||||
|             .flatten() | ||||
|             .filter { it.isDirectory || isSupportedFile(it.extension) } | ||||
|             .map { chapterFile -> | ||||
|                 SChapter.create().apply { | ||||
|                     url = "${manga.url}/${chapterFile.name}" | ||||
|                     val chapName = if (chapterFile.isDirectory) { | ||||
|                         chapterFile.name | ||||
|                     } else { | ||||
|                         chapterFile.nameWithoutExtension | ||||
|                     } | ||||
|                     val chapNameCut = chapName.replace(manga.title, "", true).trim(' ', '-', '_') | ||||
|                     name = if (chapNameCut.isEmpty()) chapName else chapNameCut | ||||
|                     date_upload = chapterFile.lastModified() | ||||
|                     ChapterRecognition.parseChapterNumber(this, manga) | ||||
|                 } | ||||
|                 .sortedWith(Comparator { c1, c2 -> | ||||
|             } | ||||
|             .sortedWith( | ||||
|                 Comparator { c1, c2 -> | ||||
|                     val c = c2.chapter_number.compareTo(c1.chapter_number) | ||||
|                     if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c | ||||
|                 }) | ||||
|                 .toList() | ||||
|                 } | ||||
|             ) | ||||
|             .toList() | ||||
|  | ||||
|         return Observable.just(chapters) | ||||
|     } | ||||
| @@ -215,16 +219,16 @@ class LocalSource(private val context: Context) : CatalogueSource { | ||||
|         return when (val format = getFormat(chapter)) { | ||||
|             is Format.Directory -> { | ||||
|                 val entry = format.file.listFiles() | ||||
|                         .sortedWith(Comparator<File> { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }) | ||||
|                         .find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } } | ||||
|                     .sortedWith(Comparator<File> { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }) | ||||
|                     .find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } } | ||||
|  | ||||
|                 entry?.let { updateCover(context, manga, it.inputStream()) } | ||||
|             } | ||||
|             is Format.Zip -> { | ||||
|                 ZipFile(format.file).use { zip -> | ||||
|                     val entry = zip.entries().toList() | ||||
|                             .sortedWith(Comparator<ZipEntry> { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }) | ||||
|                             .find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } } | ||||
|                         .sortedWith(Comparator<ZipEntry> { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }) | ||||
|                         .find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } } | ||||
|  | ||||
|                     entry?.let { updateCover(context, manga, zip.getInputStream(it)) } | ||||
|                 } | ||||
| @@ -232,8 +236,8 @@ class LocalSource(private val context: Context) : CatalogueSource { | ||||
|             is Format.Rar -> { | ||||
|                 Archive(format.file).use { archive -> | ||||
|                     val entry = archive.fileHeaders | ||||
|                             .sortedWith(Comparator<FileHeader> { f1, f2 -> f1.fileNameString.compareToCaseInsensitiveNaturalOrder(f2.fileNameString) }) | ||||
|                             .find { !it.isDirectory && ImageUtil.isImage(it.fileNameString) { archive.getInputStream(it) } } | ||||
|                         .sortedWith(Comparator<FileHeader> { f1, f2 -> f1.fileNameString.compareToCaseInsensitiveNaturalOrder(f2.fileNameString) }) | ||||
|                         .find { !it.isDirectory && ImageUtil.isImage(it.fileNameString) { archive.getInputStream(it) } } | ||||
|  | ||||
|                     entry?.let { updateCover(context, manga, archive.getInputStream(it)) } | ||||
|                 } | ||||
| @@ -241,8 +245,8 @@ class LocalSource(private val context: Context) : CatalogueSource { | ||||
|             is Format.Epub -> { | ||||
|                 EpubFile(format.file).use { epub -> | ||||
|                     val entry = epub.getImagesFromPages() | ||||
|                             .firstOrNull() | ||||
|                             ?.let { epub.getEntry(it) } | ||||
|                         .firstOrNull() | ||||
|                         ?.let { epub.getEntry(it) } | ||||
|  | ||||
|                     entry?.let { updateCover(context, manga, epub.getInputStream(it)) } | ||||
|                 } | ||||
|   | ||||
| @@ -46,7 +46,7 @@ open class SourceManager(private val context: Context) { | ||||
|     } | ||||
|  | ||||
|     private fun createInternalSources(): List<Source> = listOf( | ||||
|             LocalSource(context) | ||||
|         LocalSource(context) | ||||
|     ) | ||||
|  | ||||
|     private inner class StubSource(override val id: Long) : Source { | ||||
|   | ||||
| @@ -23,25 +23,31 @@ interface SManga : Serializable { | ||||
|     var initialized: Boolean | ||||
|  | ||||
|     fun copyFrom(other: SManga) { | ||||
|         if (other.author != null) | ||||
|         if (other.author != null) { | ||||
|             author = other.author | ||||
|         } | ||||
|  | ||||
|         if (other.artist != null) | ||||
|         if (other.artist != null) { | ||||
|             artist = other.artist | ||||
|         } | ||||
|  | ||||
|         if (other.description != null) | ||||
|         if (other.description != null) { | ||||
|             description = other.description | ||||
|         } | ||||
|  | ||||
|         if (other.genre != null) | ||||
|         if (other.genre != null) { | ||||
|             genre = other.genre | ||||
|         } | ||||
|  | ||||
|         if (other.thumbnail_url != null) | ||||
|         if (other.thumbnail_url != null) { | ||||
|             thumbnail_url = other.thumbnail_url | ||||
|         } | ||||
|  | ||||
|         status = other.status | ||||
|  | ||||
|         if (!initialized) | ||||
|         if (!initialized) { | ||||
|             initialized = other.initialized | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     companion object { | ||||
|   | ||||
| @@ -90,10 +90,10 @@ abstract class HttpSource : CatalogueSource { | ||||
|      */ | ||||
|     override fun fetchPopularManga(page: Int): Observable<MangasPage> { | ||||
|         return client.newCall(popularMangaRequest(page)) | ||||
|                 .asObservableSuccess() | ||||
|                 .map { response -> | ||||
|                     popularMangaParse(response) | ||||
|                 } | ||||
|             .asObservableSuccess() | ||||
|             .map { response -> | ||||
|                 popularMangaParse(response) | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -120,10 +120,10 @@ abstract class HttpSource : CatalogueSource { | ||||
|      */ | ||||
|     override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> { | ||||
|         return client.newCall(searchMangaRequest(page, query, filters)) | ||||
|                 .asObservableSuccess() | ||||
|                 .map { response -> | ||||
|                     searchMangaParse(response) | ||||
|                 } | ||||
|             .asObservableSuccess() | ||||
|             .map { response -> | ||||
|                 searchMangaParse(response) | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -149,10 +149,10 @@ abstract class HttpSource : CatalogueSource { | ||||
|      */ | ||||
|     override fun fetchLatestUpdates(page: Int): Observable<MangasPage> { | ||||
|         return client.newCall(latestUpdatesRequest(page)) | ||||
|                 .asObservableSuccess() | ||||
|                 .map { response -> | ||||
|                     latestUpdatesParse(response) | ||||
|                 } | ||||
|             .asObservableSuccess() | ||||
|             .map { response -> | ||||
|                 latestUpdatesParse(response) | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -177,10 +177,10 @@ abstract class HttpSource : CatalogueSource { | ||||
|      */ | ||||
|     override fun fetchMangaDetails(manga: SManga): Observable<SManga> { | ||||
|         return client.newCall(mangaDetailsRequest(manga)) | ||||
|                 .asObservableSuccess() | ||||
|                 .map { response -> | ||||
|                     mangaDetailsParse(response).apply { initialized = true } | ||||
|                 } | ||||
|             .asObservableSuccess() | ||||
|             .map { response -> | ||||
|                 mangaDetailsParse(response).apply { initialized = true } | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -209,10 +209,10 @@ abstract class HttpSource : CatalogueSource { | ||||
|     override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> { | ||||
|         return if (manga.status != SManga.LICENSED) { | ||||
|             client.newCall(chapterListRequest(manga)) | ||||
|                     .asObservableSuccess() | ||||
|                     .map { response -> | ||||
|                         chapterListParse(response) | ||||
|                     } | ||||
|                 .asObservableSuccess() | ||||
|                 .map { response -> | ||||
|                     chapterListParse(response) | ||||
|                 } | ||||
|         } else { | ||||
|             Observable.error(Exception("Licensed - No chapters to show")) | ||||
|         } | ||||
| @@ -242,10 +242,10 @@ abstract class HttpSource : CatalogueSource { | ||||
|      */ | ||||
|     override fun fetchPageList(chapter: SChapter): Observable<List<Page>> { | ||||
|         return client.newCall(pageListRequest(chapter)) | ||||
|                 .asObservableSuccess() | ||||
|                 .map { response -> | ||||
|                     pageListParse(response) | ||||
|                 } | ||||
|             .asObservableSuccess() | ||||
|             .map { response -> | ||||
|                 pageListParse(response) | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -273,8 +273,8 @@ abstract class HttpSource : CatalogueSource { | ||||
|      */ | ||||
|     open fun fetchImageUrl(page: Page): Observable<String> { | ||||
|         return client.newCall(imageUrlRequest(page)) | ||||
|                 .asObservableSuccess() | ||||
|                 .map { imageUrlParse(it) } | ||||
|             .asObservableSuccess() | ||||
|             .map { imageUrlParse(it) } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -301,7 +301,7 @@ abstract class HttpSource : CatalogueSource { | ||||
|      */ | ||||
|     fun fetchImage(page: Page): Observable<Response> { | ||||
|         return client.newCallWithProgress(imageRequest(page), page) | ||||
|                 .asObservableSuccess() | ||||
|             .asObservableSuccess() | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -343,10 +343,12 @@ abstract class HttpSource : CatalogueSource { | ||||
|         return try { | ||||
|             val uri = URI(orig) | ||||
|             var out = uri.path | ||||
|             if (uri.query != null) | ||||
|             if (uri.query != null) { | ||||
|                 out += "?" + uri.query | ||||
|             if (uri.fragment != null) | ||||
|             } | ||||
|             if (uri.fragment != null) { | ||||
|                 out += "#" + uri.fragment | ||||
|             } | ||||
|             out | ||||
|         } catch (e: URISyntaxException) { | ||||
|             orig | ||||
|   | ||||
| @@ -6,20 +6,20 @@ import rx.Observable | ||||
| fun HttpSource.getImageUrl(page: Page): Observable<Page> { | ||||
|     page.status = Page.LOAD_PAGE | ||||
|     return fetchImageUrl(page) | ||||
|             .doOnError { page.status = Page.ERROR } | ||||
|             .onErrorReturn { null } | ||||
|             .doOnNext { page.imageUrl = it } | ||||
|             .map { page } | ||||
|         .doOnError { page.status = Page.ERROR } | ||||
|         .onErrorReturn { null } | ||||
|         .doOnNext { page.imageUrl = it } | ||||
|         .map { page } | ||||
| } | ||||
|  | ||||
| fun HttpSource.fetchAllImageUrlsFromPageList(pages: List<Page>): Observable<Page> { | ||||
|     return Observable.from(pages) | ||||
|             .filter { !it.imageUrl.isNullOrEmpty() } | ||||
|             .mergeWith(fetchRemainingImageUrlsFromPageList(pages)) | ||||
|         .filter { !it.imageUrl.isNullOrEmpty() } | ||||
|         .mergeWith(fetchRemainingImageUrlsFromPageList(pages)) | ||||
| } | ||||
|  | ||||
| fun HttpSource.fetchRemainingImageUrlsFromPageList(pages: List<Page>): Observable<Page> { | ||||
|     return Observable.from(pages) | ||||
|             .filter { it.imageUrl.isNullOrEmpty() } | ||||
|             .concatMap { getImageUrl(it) } | ||||
|         .filter { it.imageUrl.isNullOrEmpty() } | ||||
|         .concatMap { getImageUrl(it) } | ||||
| } | ||||
|   | ||||
| @@ -59,17 +59,19 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() { | ||||
|     } | ||||
|  | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         setTheme(when (preferences.themeMode().get()) { | ||||
|             Values.THEME_MODE_SYSTEM -> { | ||||
|                 if (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES) { | ||||
|                     darkTheme | ||||
|                 } else { | ||||
|                     lightTheme | ||||
|         setTheme( | ||||
|             when (preferences.themeMode().get()) { | ||||
|                 Values.THEME_MODE_SYSTEM -> { | ||||
|                     if (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES) { | ||||
|                         darkTheme | ||||
|                     } else { | ||||
|                         lightTheme | ||||
|                     } | ||||
|                 } | ||||
|                 Values.THEME_MODE_DARK -> darkTheme | ||||
|                 else -> lightTheme | ||||
|             } | ||||
|             Values.THEME_MODE_DARK -> darkTheme | ||||
|             else -> lightTheme | ||||
|         }) | ||||
|         ) | ||||
|  | ||||
|         super.onCreate(savedInstanceState) | ||||
|  | ||||
|   | ||||
| @@ -15,8 +15,9 @@ import kotlinx.android.extensions.LayoutContainer | ||||
| import kotlinx.android.synthetic.clearFindViewByIdCache | ||||
| import timber.log.Timber | ||||
|  | ||||
| abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) : RestoreViewOnCreateController(bundle), | ||||
|         LayoutContainer { | ||||
| abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) : | ||||
|     RestoreViewOnCreateController(bundle), | ||||
|     LayoutContainer { | ||||
|  | ||||
|     lateinit var binding: VB | ||||
|  | ||||
|   | ||||
| @@ -30,6 +30,6 @@ fun Controller.requestPermissionsSafe(permissions: Array<String>, requestCode: I | ||||
|  | ||||
| fun Controller.withFadeTransaction(): RouterTransaction { | ||||
|     return RouterTransaction.with(this) | ||||
|             .pushChangeHandler(FadeChangeHandler()) | ||||
|             .popChangeHandler(FadeChangeHandler()) | ||||
|         .pushChangeHandler(FadeChangeHandler()) | ||||
|         .popChangeHandler(FadeChangeHandler()) | ||||
| } | ||||
|   | ||||
| @@ -87,10 +87,12 @@ abstract class DialogController : RestoreViewOnCreateController { | ||||
|      */ | ||||
|     fun showDialog(router: Router, tag: String?) { | ||||
|         dismissed = false | ||||
|         router.pushController(RouterTransaction.with(this) | ||||
|         router.pushController( | ||||
|             RouterTransaction.with(this) | ||||
|                 .pushChangeHandler(SimpleSwapChangeHandler(false)) | ||||
|                 .popChangeHandler(SimpleSwapChangeHandler(false)) | ||||
|                 .tag(tag)) | ||||
|                 .tag(tag) | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -44,12 +44,10 @@ abstract class RxController<VB : ViewBinding>(bundle: Bundle? = null) : BaseCont | ||||
|     } | ||||
|  | ||||
|     fun <T> Observable<T>.subscribeUntilDetach(): Subscription { | ||||
|  | ||||
|         return subscribe().also { untilDetachSubscriptions.add(it) } | ||||
|     } | ||||
|  | ||||
|     fun <T> Observable<T>.subscribeUntilDetach(onNext: (T) -> Unit): Subscription { | ||||
|  | ||||
|         return subscribe(onNext).also { untilDetachSubscriptions.add(it) } | ||||
|     } | ||||
|  | ||||
| @@ -57,7 +55,6 @@ abstract class RxController<VB : ViewBinding>(bundle: Bundle? = null) : BaseCont | ||||
|         onNext: (T) -> Unit, | ||||
|         onError: (Throwable) -> Unit | ||||
|     ): Subscription { | ||||
|  | ||||
|         return subscribe(onNext, onError).also { untilDetachSubscriptions.add(it) } | ||||
|     } | ||||
|  | ||||
| @@ -66,17 +63,14 @@ abstract class RxController<VB : ViewBinding>(bundle: Bundle? = null) : BaseCont | ||||
|         onError: (Throwable) -> Unit, | ||||
|         onCompleted: () -> Unit | ||||
|     ): Subscription { | ||||
|  | ||||
|         return subscribe(onNext, onError, onCompleted).also { untilDetachSubscriptions.add(it) } | ||||
|     } | ||||
|  | ||||
|     fun <T> Observable<T>.subscribeUntilDestroy(): Subscription { | ||||
|  | ||||
|         return subscribe().also { untilDestroySubscriptions.add(it) } | ||||
|     } | ||||
|  | ||||
|     fun <T> Observable<T>.subscribeUntilDestroy(onNext: (T) -> Unit): Subscription { | ||||
|  | ||||
|         return subscribe(onNext).also { untilDestroySubscriptions.add(it) } | ||||
|     } | ||||
|  | ||||
| @@ -84,7 +78,6 @@ abstract class RxController<VB : ViewBinding>(bundle: Bundle? = null) : BaseCont | ||||
|         onNext: (T) -> Unit, | ||||
|         onError: (Throwable) -> Unit | ||||
|     ): Subscription { | ||||
|  | ||||
|         return subscribe(onNext, onError).also { untilDestroySubscriptions.add(it) } | ||||
|     } | ||||
|  | ||||
| @@ -93,7 +86,6 @@ abstract class RxController<VB : ViewBinding>(bundle: Bundle? = null) : BaseCont | ||||
|         onError: (Throwable) -> Unit, | ||||
|         onCompleted: () -> Unit | ||||
|     ): Subscription { | ||||
|  | ||||
|         return subscribe(onNext, onError, onCompleted).also { untilDestroySubscriptions.add(it) } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -59,11 +59,11 @@ open class BasePresenter<V> : RxPresenter<V>() { | ||||
|  | ||||
|         override fun call(observable: Observable<T>): Observable<Delivery<View, T>> { | ||||
|             return observable | ||||
|                     .materialize() | ||||
|                     .filter { notification -> !notification.isOnCompleted } | ||||
|                     .flatMap { notification -> | ||||
|                         view.take(1).filter { it != null }.map { Delivery(it, notification) } | ||||
|                     } | ||||
|                 .materialize() | ||||
|                 .filter { notification -> !notification.isOnCompleted } | ||||
|                 .flatMap { notification -> | ||||
|                     view.take(1).filter { it != null }.map { Delivery(it, notification) } | ||||
|                 } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -8,7 +8,7 @@ import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
|  * @param controller The containing controller. | ||||
|  */ | ||||
| class CategoryAdapter(controller: CategoryController) : | ||||
|         FlexibleAdapter<CategoryItem>(null, controller, true) { | ||||
|     FlexibleAdapter<CategoryItem>(null, controller, true) { | ||||
|  | ||||
|     /** | ||||
|      * Listener called when an item of the list is released. | ||||
|   | ||||
| @@ -25,14 +25,15 @@ import reactivecircus.flowbinding.android.view.clicks | ||||
| /** | ||||
|  * Controller to manage the categories for the users' library. | ||||
|  */ | ||||
| class CategoryController : NucleusController<CategoriesControllerBinding, CategoryPresenter>(), | ||||
|         ActionMode.Callback, | ||||
|         FlexibleAdapter.OnItemClickListener, | ||||
|         FlexibleAdapter.OnItemLongClickListener, | ||||
|         CategoryAdapter.OnItemReleaseListener, | ||||
|         CategoryCreateDialog.Listener, | ||||
|         CategoryRenameDialog.Listener, | ||||
|         UndoHelper.OnActionListener { | ||||
| class CategoryController : | ||||
|     NucleusController<CategoriesControllerBinding, CategoryPresenter>(), | ||||
|     ActionMode.Callback, | ||||
|     FlexibleAdapter.OnItemClickListener, | ||||
|     FlexibleAdapter.OnItemLongClickListener, | ||||
|     CategoryAdapter.OnItemReleaseListener, | ||||
|     CategoryCreateDialog.Listener, | ||||
|     CategoryRenameDialog.Listener, | ||||
|     UndoHelper.OnActionListener { | ||||
|  | ||||
|     /** | ||||
|      * Object used to show ActionMode toolbar. | ||||
| @@ -176,8 +177,10 @@ class CategoryController : NucleusController<CategoriesControllerBinding, Catego | ||||
|         when (item.itemId) { | ||||
|             R.id.action_delete -> { | ||||
|                 undoHelper = UndoHelper(adapter, this) | ||||
|                 undoHelper?.start(adapter.selectedPositions, view!!, | ||||
|                         R.string.snack_categories_deleted, R.string.action_undo, 3000) | ||||
|                 undoHelper?.start( | ||||
|                     adapter.selectedPositions, view!!, | ||||
|                     R.string.snack_categories_deleted, R.string.action_undo, 3000 | ||||
|                 ) | ||||
|  | ||||
|                 mode.finish() | ||||
|             } | ||||
|   | ||||
| @@ -31,17 +31,17 @@ class CategoryCreateDialog<T>(bundle: Bundle? = null) : DialogController(bundle) | ||||
|      */ | ||||
|     override fun onCreateDialog(savedViewState: Bundle?): Dialog { | ||||
|         return MaterialDialog(activity!!) | ||||
|                 .title(R.string.action_add_category) | ||||
|                 .negativeButton(android.R.string.cancel) | ||||
|                 .input( | ||||
|                     hint = resources?.getString(R.string.name), | ||||
|                     prefill = currentName | ||||
|                 ) { _, input -> | ||||
|                     currentName = input.toString() | ||||
|                 } | ||||
|                 .positiveButton(android.R.string.ok) { | ||||
|                     (targetController as? Listener)?.createCategory(currentName) | ||||
|                 } | ||||
|             .title(R.string.action_add_category) | ||||
|             .negativeButton(android.R.string.cancel) | ||||
|             .input( | ||||
|                 hint = resources?.getString(R.string.name), | ||||
|                 prefill = currentName | ||||
|             ) { _, input -> | ||||
|                 currentName = input.toString() | ||||
|             } | ||||
|             .positiveButton(android.R.string.ok) { | ||||
|                 (targetController as? Listener)?.createCategory(currentName) | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     interface Listener { | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user