Linting fixes

This commit is contained in:
arkon 2020-04-25 14:24:45 -04:00
parent 4da760d614
commit 3f63b320c4
272 changed files with 4167 additions and 3602 deletions

View File

@ -23,12 +23,12 @@ import uy.kohesive.injekt.injectLazy
import uy.kohesive.injekt.registry.default.DefaultRegistrar import uy.kohesive.injekt.registry.default.DefaultRegistrar
@AcraCore( @AcraCore(
buildConfigClass = BuildConfig::class, buildConfigClass = BuildConfig::class,
excludeMatchingSharedPreferencesKeys = [".*username.*", ".*password.*", ".*token.*"] excludeMatchingSharedPreferencesKeys = [".*username.*", ".*password.*", ".*token.*"]
) )
@AcraHttpSender( @AcraHttpSender(
uri = "https://tachiyomi.kanade.eu/crash_report", uri = "https://tachiyomi.kanade.eu/crash_report",
httpMethod = HttpSender.Method.PUT httpMethod = HttpSender.Method.PUT
) )
open class App : Application(), LifecycleObserver { open class App : Application(), LifecycleObserver {

View File

@ -22,7 +22,6 @@ import uy.kohesive.injekt.api.get
class AppModule(val app: Application) : InjektModule { class AppModule(val app: Application) : InjektModule {
override fun InjektRegistrar.registerInjectables() { override fun InjektRegistrar.registerInjectables() {
addSingleton(app) addSingleton(app)
addSingletonFactory { PreferencesHelper(app) } addSingletonFactory { PreferencesHelper(app) }

View File

@ -13,7 +13,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class BackupCreatorJob(private val context: Context, workerParams: WorkerParameters) : class BackupCreatorJob(private val context: Context, workerParams: WorkerParameters) :
Worker(context, workerParams) { Worker(context, workerParams) {
override fun doWork(): Result { override fun doWork(): Result {
val preferences = Injekt.get<PreferencesHelper>() val preferences = Injekt.get<PreferencesHelper>()
@ -32,10 +32,11 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
val interval = prefInterval ?: preferences.backupInterval().get() val interval = prefInterval ?: preferences.backupInterval().get()
if (interval > 0) { if (interval > 0) {
val request = PeriodicWorkRequestBuilder<BackupCreatorJob>( val request = PeriodicWorkRequestBuilder<BackupCreatorJob>(
interval.toLong(), TimeUnit.HOURS, interval.toLong(), TimeUnit.HOURS,
10, TimeUnit.MINUTES) 10, TimeUnit.MINUTES
.addTag(TAG) )
.build() .addTag(TAG)
.build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request) WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request)
} else { } else {

View File

@ -85,7 +85,8 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
private fun initParser(): Gson = when (version) { private fun initParser(): Gson = when (version) {
1 -> GsonBuilder().create() 1 -> GsonBuilder().create()
2 -> GsonBuilder() 2 ->
GsonBuilder()
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build()) .registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build()) .registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build()) .registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
@ -142,21 +143,21 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
val numberOfBackups = numberOfBackups() val numberOfBackups = numberOfBackups()
val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.json""") val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.json""")
dir.listFiles { _, filename -> backupRegex.matches(filename) } dir.listFiles { _, filename -> backupRegex.matches(filename) }
.orEmpty() .orEmpty()
.sortedByDescending { it.name } .sortedByDescending { it.name }
.drop(numberOfBackups - 1) .drop(numberOfBackups - 1)
.forEach { it.delete() } .forEach { it.delete() }
// Create new file to place backup // Create new file to place backup
val newFile = dir.createFile(Backup.getDefaultFilename()) val newFile = dir.createFile(Backup.getDefaultFilename())
?: throw Exception("Couldn't create backup file") ?: throw Exception("Couldn't create backup file")
newFile.openOutputStream().bufferedWriter().use { newFile.openOutputStream().bufferedWriter().use {
parser.toJson(root, it) parser.toJson(root, it)
} }
} else { } else {
val file = UniFile.fromUri(context, uri) val file = UniFile.fromUri(context, uri)
?: throw Exception("Couldn't create backup file") ?: throw Exception("Couldn't create backup file")
file.openOutputStream().bufferedWriter().use { file.openOutputStream().bufferedWriter().use {
parser.toJson(root, it) 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> { fun restoreMangaFetchObservable(source: Source, manga: Manga): Observable<Manga> {
return source.fetchMangaDetails(manga) return source.fetchMangaDetails(manga)
.map { networkManga -> .map { networkManga ->
manga.copyFrom(networkManga) manga.copyFrom(networkManga)
manga.favorite = true manga.favorite = true
manga.initialized = true manga.initialized = true
manga.id = insertManga(manga) manga.id = insertManga(manga)
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>>> { fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
return source.fetchChapterList(manga) return source.fetchChapterList(manga)
.map { syncChaptersWithSource(databaseHelper, it, manga, source) } .map { syncChaptersWithSource(databaseHelper, it, manga, source) }
.doOnNext { pair -> .doOnNext { pair ->
if (pair.first.isNotEmpty()) { if (pair.first.isNotEmpty()) {
chapters.forEach { it.manga_id = manga.id } chapters.forEach { it.manga_id = manga.id }
insertChapters(chapters) insertChapters(chapters)
}
} }
}
} }
/** /**
@ -442,8 +443,9 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking() val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
// Return if fetch is needed // Return if fetch is needed
if (dbChapters.isEmpty() || dbChapters.size < chapters.size) if (dbChapters.isEmpty() || dbChapters.size < chapters.size) {
return false return false
}
for (chapter in chapters) { for (chapter in chapters) {
val pos = dbChapters.indexOf(chapter) val pos = dbChapters.indexOf(chapter)
@ -468,7 +470,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
* @return [Manga], null if not found * @return [Manga], null if not found
*/ */
internal fun getMangaFromDatabase(manga: Manga): Manga? = 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 * Returns list containing manga from library
@ -476,7 +478,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
* @return [Manga] from library * @return [Manga] from library
*/ */
internal fun getFavoriteManga(): List<Manga> = internal fun getFavoriteManga(): List<Manga> =
databaseHelper.getFavoriteMangas().executeAsBlocking() databaseHelper.getFavoriteMangas().executeAsBlocking()
/** /**
* Inserts manga and returns id * 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 * @return id of [Manga], null if not found
*/ */
internal fun insertManga(manga: Manga): Long? = internal fun insertManga(manga: Manga): Long? =
databaseHelper.insertManga(manga).executeAsBlocking().insertedId() databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
/** /**
* Inserts list of chapters * Inserts list of chapters

View File

@ -60,7 +60,7 @@ class BackupRestoreService : Service() {
* @return true if the service is running, false otherwise. * @return true if the service is running, false otherwise.
*/ */
private fun isRunning(context: Context): Boolean = 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 * Starts a service to restore a backup from Json
@ -143,7 +143,8 @@ class BackupRestoreService : Service() {
startForeground(Notifications.ID_RESTORE, notifier.showRestoreProgress().build()) startForeground(Notifications.ID_RESTORE, notifier.showRestoreProgress().build())
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock( wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, "BackupRestoreService:WakeLock") PowerManager.PARTIAL_WAKE_LOCK, "BackupRestoreService:WakeLock"
)
wakeLock.acquire() wakeLock.acquire()
} }
@ -182,12 +183,13 @@ class BackupRestoreService : Service() {
subscription?.unsubscribe() subscription?.unsubscribe()
subscription = Observable.using( subscription = Observable.using(
{ db.lowLevel().beginTransaction() }, { db.lowLevel().beginTransaction() },
{ getRestoreObservable(uri).doOnNext { db.lowLevel().setTransactionSuccessful() } }, { getRestoreObservable(uri).doOnNext { db.lowLevel().setTransactionSuccessful() } },
{ executor.execute { db.lowLevel().endTransaction() } }) { executor.execute { db.lowLevel().endTransaction() } }
.doAfterTerminate { stopSelf(startId) } )
.subscribeOn(Schedulers.from(executor)) .doAfterTerminate { stopSelf(startId) }
.subscribe() .subscribeOn(Schedulers.from(executor))
.subscribe()
return START_NOT_STICKY return START_NOT_STICKY
} }
@ -202,79 +204,87 @@ class BackupRestoreService : Service() {
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
return Observable.just(Unit) return Observable.just(Unit)
.map { .map {
val reader = JsonReader(contentResolver.openInputStream(uri)!!.bufferedReader()) val reader = JsonReader(contentResolver.openInputStream(uri)!!.bufferedReader())
val json = JsonParser.parseReader(reader).asJsonObject val json = JsonParser.parseReader(reader).asJsonObject
// Get parser version // Get parser version
val version = json.get(VERSION)?.asInt ?: 1 val version = json.get(VERSION)?.asInt ?: 1
// Initialize manager // Initialize manager
backupManager = BackupManager(this, version) backupManager = BackupManager(this, version)
val mangasJson = json.get(MANGAS).asJsonArray val mangasJson = json.get(MANGAS).asJsonArray
restoreAmount = mangasJson.size() + 1 // +1 for categories restoreAmount = mangasJson.size() + 1 // +1 for categories
restoreProgress = 0 restoreProgress = 0
errors.clear() errors.clear()
// Restore categories // Restore categories
json.get(CATEGORIES)?.let { json.get(CATEGORIES)?.let {
backupManager.restoreCategories(it.asJsonArray) backupManager.restoreCategories(it.asJsonArray)
restoreProgress += 1 restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, "Categories added") showRestoreProgress(restoreProgress, restoreAmount, "Categories added")
}
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) mangasJson
if (observable != null) { }
observable .flatMap { Observable.from(it) }
} else { .concatMap {
errors.add(Date() to "${manga.title} - ${getString(R.string.source_not_found)}") val obj = it.asJsonObject
restoreProgress += 1 val manga = backupManager.parser.fromJson<MangaImpl>(obj.get(MANGA))
val content = getString(R.string.dialog_restoring_source_not_found, manga.title.chop(15)) val chapters = backupManager.parser.fromJson<List<ChapterImpl>>(
showRestoreProgress(restoreProgress, restoreAmount, manga.title, content) obj.get(CHAPTERS)
Observable.just(manga) ?: 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 { .toList()
val endTime = System.currentTimeMillis() .doOnNext {
val time = endTime - startTime val endTime = System.currentTimeMillis()
val logFile = writeErrorLog() val time = endTime - startTime
val completeIntent = Intent(BackupConst.INTENT_FILTER).apply { val logFile = writeErrorLog()
putExtra(BackupConst.EXTRA_TIME, time) val completeIntent = Intent(BackupConst.INTENT_FILTER).apply {
putExtra(BackupConst.EXTRA_ERRORS, errors.size) putExtra(BackupConst.EXTRA_TIME, time)
putExtra(BackupConst.EXTRA_ERROR_FILE_PATH, logFile.parent) putExtra(BackupConst.EXTRA_ERRORS, errors.size)
putExtra(BackupConst.EXTRA_ERROR_FILE, logFile.name) putExtra(BackupConst.EXTRA_ERROR_FILE_PATH, logFile.parent)
putExtra(BackupConst.ACTION, BackupConst.ACTION_RESTORE_COMPLETED) putExtra(BackupConst.EXTRA_ERROR_FILE, logFile.name)
} putExtra(BackupConst.ACTION, BackupConst.ACTION_RESTORE_COMPLETED)
sendLocalBroadcast(completeIntent)
} }
.doOnError { error -> sendLocalBroadcast(completeIntent)
Timber.e(error) }
writeErrorLog() .doOnError { error ->
val errorIntent = Intent(BackupConst.INTENT_FILTER).apply { Timber.e(error)
putExtra(BackupConst.ACTION, BackupConst.ACTION_RESTORE_ERROR) writeErrorLog()
putExtra(BackupConst.EXTRA_ERROR_MESSAGE, error.message) val errorIntent = Intent(BackupConst.INTENT_FILTER).apply {
} putExtra(BackupConst.ACTION, BackupConst.ACTION_RESTORE_ERROR)
sendLocalBroadcast(errorIntent) putExtra(BackupConst.EXTRA_ERROR_MESSAGE, error.message)
} }
.onErrorReturn { emptyList() } sendLocalBroadcast(errorIntent)
}
.onErrorReturn { emptyList() }
} }
/** /**
@ -347,28 +357,28 @@ class BackupRestoreService : Service() {
tracks: List<Track> tracks: List<Track>
): Observable<Manga> { ): Observable<Manga> {
return backupManager.restoreMangaFetchObservable(source, manga) return backupManager.restoreMangaFetchObservable(source, manga)
.onErrorReturn { .onErrorReturn {
errors.add(Date() to "${manga.title} - ${it.message}") errors.add(Date() to "${manga.title} - ${it.message}")
manga manga
} }
.filter { it.id != null } .filter { it.id != null }
.flatMap { .flatMap {
chapterFetchObservable(source, it, chapters) chapterFetchObservable(source, it, chapters)
// Convert to the manga that contains new chapters. // Convert to the manga that contains new chapters.
.map { manga } .map { manga }
} }
.doOnNext { .doOnNext {
restoreExtraForManga(it, categories, history, tracks) restoreExtraForManga(it, categories, history, tracks)
} }
.flatMap { .flatMap {
trackingFetchObservable(it, tracks) trackingFetchObservable(it, tracks)
// Convert to the manga that contains new chapters. // Convert to the manga that contains new chapters.
.map { manga } .map { manga }
} }
.doOnCompleted { .doOnCompleted {
restoreProgress += 1 restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, manga.title) showRestoreProgress(restoreProgress, restoreAmount, manga.title)
} }
} }
private fun mangaNoFetchObservable( private fun mangaNoFetchObservable(
@ -379,28 +389,27 @@ class BackupRestoreService : Service() {
history: List<DHistory>, history: List<DHistory>,
tracks: List<Track> tracks: List<Track>
): Observable<Manga> { ): Observable<Manga> {
return Observable.just(backupManga) return Observable.just(backupManga)
.flatMap { manga -> .flatMap { manga ->
if (!backupManager.restoreChaptersForManga(manga, chapters)) { if (!backupManager.restoreChaptersForManga(manga, chapters)) {
chapterFetchObservable(source, manga, chapters) chapterFetchObservable(source, manga, chapters)
.map { manga } .map { manga }
} else { } else {
Observable.just(manga) 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)
} }
}
.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>) { 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>>> { private fun chapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
return backupManager.restoreChapterFetchObservable(source, manga, chapters) return backupManager.restoreChapterFetchObservable(source, manga, chapters)
// If there's any error, return empty update and continue. // If there's any error, return empty update and continue.
.onErrorReturn { .onErrorReturn {
errors.add(Date() to "${manga.title} - ${it.message}") errors.add(Date() to "${manga.title} - ${it.message}")
Pair(emptyList(), emptyList()) Pair(emptyList(), emptyList())
} }
} }
/** /**
@ -438,20 +447,20 @@ class BackupRestoreService : Service() {
*/ */
private fun trackingFetchObservable(manga: Manga, tracks: List<Track>): Observable<Track> { private fun trackingFetchObservable(manga: Manga, tracks: List<Track>): Observable<Track> {
return Observable.from(tracks) return Observable.from(tracks)
.concatMap { track -> .concatMap { track ->
val service = trackManager.getService(track.sync_id) val service = trackManager.getService(track.sync_id)
if (service != null && service.isLogged) { if (service != null && service.isLogged) {
service.refresh(track) service.refresh(track)
.doOnNext { db.insertTrack(it).executeAsBlocking() } .doOnNext { db.insertTrack(it).executeAsBlocking() }
.onErrorReturn { .onErrorReturn {
errors.add(Date() to "${manga.title} - ${it.message}") errors.add(Date() to "${manga.title} - ${it.message}")
track track
} }
} else { } else {
errors.add(Date() to "${manga.title} - ${service?.name} not logged in") errors.add(Date() to "${manga.title} - ${service?.name} not logged in")
Observable.empty() Observable.empty()
}
} }
}
} }
/** /**

View File

@ -46,10 +46,12 @@ class ChapterCache(private val context: Context) {
private val gson: Gson by injectLazy() private val gson: Gson by injectLazy()
/** Cache class used for cache management. */ /** Cache class used for cache management. */
private val diskCache = DiskLruCache.open(File(context.cacheDir, PARAMETER_CACHE_DIRECTORY), private val diskCache = DiskLruCache.open(
PARAMETER_APP_VERSION, File(context.cacheDir, PARAMETER_CACHE_DIRECTORY),
PARAMETER_VALUE_COUNT, PARAMETER_APP_VERSION,
PARAMETER_CACHE_SIZE) PARAMETER_VALUE_COUNT,
PARAMETER_CACHE_SIZE
)
/** /**
* Returns directory of cache. * Returns directory of cache.
@ -77,8 +79,9 @@ class ChapterCache(private val context: Context) {
*/ */
fun removeFileFromCache(file: String): Boolean { fun removeFileFromCache(file: String): Boolean {
// Make sure we don't delete the journal file (keeps track of cache). // 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 false
}
return try { return try {
// Remove the extension from the file to get the key of the cache // Remove the extension from the file to get the key of the cache

View File

@ -21,7 +21,7 @@ class CoverCache(private val context: Context) {
* Cache directory used for cache management. * Cache directory used for cache management.
*/ */
private val cacheDir = context.getExternalFilesDir("covers") 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. * Returns the cover from cache.
@ -56,8 +56,9 @@ class CoverCache(private val context: Context) {
*/ */
fun deleteFromCache(thumbnailUrl: String?): Boolean { fun deleteFromCache(thumbnailUrl: String?): Boolean {
// Check if url is empty. // Check if url is empty.
if (thumbnailUrl.isNullOrEmpty()) if (thumbnailUrl.isNullOrEmpty()) {
return false return false
}
// Remove file. // Remove file.
val file = getCoverFile(thumbnailUrl) val file = getCoverFile(thumbnailUrl)

View File

@ -30,19 +30,19 @@ open class DatabaseHelper(context: Context) :
MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries { MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries {
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context) private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
.name(DbOpenCallback.DATABASE_NAME) .name(DbOpenCallback.DATABASE_NAME)
.callback(DbOpenCallback()) .callback(DbOpenCallback())
.build() .build()
override val db = DefaultStorIOSQLite.builder() override val db = DefaultStorIOSQLite.builder()
.sqliteOpenHelper(RequerySQLiteOpenHelperFactory().create(configuration)) .sqliteOpenHelper(RequerySQLiteOpenHelperFactory().create(configuration))
.addTypeMapping(Manga::class.java, MangaTypeMapping()) .addTypeMapping(Manga::class.java, MangaTypeMapping())
.addTypeMapping(Chapter::class.java, ChapterTypeMapping()) .addTypeMapping(Chapter::class.java, ChapterTypeMapping())
.addTypeMapping(Track::class.java, TrackTypeMapping()) .addTypeMapping(Track::class.java, TrackTypeMapping())
.addTypeMapping(Category::class.java, CategoryTypeMapping()) .addTypeMapping(Category::class.java, CategoryTypeMapping())
.addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping()) .addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping())
.addTypeMapping(History::class.java, HistoryTypeMapping()) .addTypeMapping(History::class.java, HistoryTypeMapping())
.build() .build()
inline fun inTransaction(block: () -> Unit) = db.inTransaction(block) inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)

View File

@ -44,8 +44,10 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
db.execSQL(ChapterTable.sourceOrderUpdateQuery) db.execSQL(ChapterTable.sourceOrderUpdateQuery)
// Fix kissmanga covers after supporting cloudflare // Fix kissmanga covers after supporting cloudflare
db.execSQL("""UPDATE mangas SET thumbnail_url = db.execSQL(
REPLACE(thumbnail_url, '93.174.95.110', 'kissmanga.com') WHERE source = 4""") """UPDATE mangas SET thumbnail_url =
REPLACE(thumbnail_url, '93.174.95.110', 'kissmanga.com') WHERE source = 4"""
)
} }
if (oldVersion < 3) { if (oldVersion < 3) {
// Initialize history tables // Initialize history tables

View File

@ -18,22 +18,22 @@ import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_ORDER
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.TABLE import eu.kanade.tachiyomi.data.database.tables.CategoryTable.TABLE
class CategoryTypeMapping : SQLiteTypeMapping<Category>( class CategoryTypeMapping : SQLiteTypeMapping<Category>(
CategoryPutResolver(), CategoryPutResolver(),
CategoryGetResolver(), CategoryGetResolver(),
CategoryDeleteResolver() CategoryDeleteResolver()
) )
class CategoryPutResolver : DefaultPutResolver<Category>() { class CategoryPutResolver : DefaultPutResolver<Category>() {
override fun mapToInsertQuery(obj: Category) = InsertQuery.builder() override fun mapToInsertQuery(obj: Category) = InsertQuery.builder()
.table(TABLE) .table(TABLE)
.build() .build()
override fun mapToUpdateQuery(obj: Category) = UpdateQuery.builder() override fun mapToUpdateQuery(obj: Category) = UpdateQuery.builder()
.table(TABLE) .table(TABLE)
.where("$COL_ID = ?") .where("$COL_ID = ?")
.whereArgs(obj.id) .whereArgs(obj.id)
.build() .build()
override fun mapToContentValues(obj: Category) = ContentValues(4).apply { override fun mapToContentValues(obj: Category) = ContentValues(4).apply {
put(COL_ID, obj.id) put(COL_ID, obj.id)
@ -56,8 +56,8 @@ class CategoryGetResolver : DefaultGetResolver<Category>() {
class CategoryDeleteResolver : DefaultDeleteResolver<Category>() { class CategoryDeleteResolver : DefaultDeleteResolver<Category>() {
override fun mapToDeleteQuery(obj: Category) = DeleteQuery.builder() override fun mapToDeleteQuery(obj: Category) = DeleteQuery.builder()
.table(TABLE) .table(TABLE)
.where("$COL_ID = ?") .where("$COL_ID = ?")
.whereArgs(obj.id) .whereArgs(obj.id)
.build() .build()
} }

View File

@ -26,22 +26,22 @@ import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_URL
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.TABLE import eu.kanade.tachiyomi.data.database.tables.ChapterTable.TABLE
class ChapterTypeMapping : SQLiteTypeMapping<Chapter>( class ChapterTypeMapping : SQLiteTypeMapping<Chapter>(
ChapterPutResolver(), ChapterPutResolver(),
ChapterGetResolver(), ChapterGetResolver(),
ChapterDeleteResolver() ChapterDeleteResolver()
) )
class ChapterPutResolver : DefaultPutResolver<Chapter>() { class ChapterPutResolver : DefaultPutResolver<Chapter>() {
override fun mapToInsertQuery(obj: Chapter) = InsertQuery.builder() override fun mapToInsertQuery(obj: Chapter) = InsertQuery.builder()
.table(TABLE) .table(TABLE)
.build() .build()
override fun mapToUpdateQuery(obj: Chapter) = UpdateQuery.builder() override fun mapToUpdateQuery(obj: Chapter) = UpdateQuery.builder()
.table(TABLE) .table(TABLE)
.where("$COL_ID = ?") .where("$COL_ID = ?")
.whereArgs(obj.id) .whereArgs(obj.id)
.build() .build()
override fun mapToContentValues(obj: Chapter) = ContentValues(11).apply { override fun mapToContentValues(obj: Chapter) = ContentValues(11).apply {
put(COL_ID, obj.id) put(COL_ID, obj.id)
@ -80,8 +80,8 @@ class ChapterGetResolver : DefaultGetResolver<Chapter>() {
class ChapterDeleteResolver : DefaultDeleteResolver<Chapter>() { class ChapterDeleteResolver : DefaultDeleteResolver<Chapter>() {
override fun mapToDeleteQuery(obj: Chapter) = DeleteQuery.builder() override fun mapToDeleteQuery(obj: Chapter) = DeleteQuery.builder()
.table(TABLE) .table(TABLE)
.where("$COL_ID = ?") .where("$COL_ID = ?")
.whereArgs(obj.id) .whereArgs(obj.id)
.build() .build()
} }

View File

@ -18,22 +18,22 @@ import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_TIME_READ
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.TABLE import eu.kanade.tachiyomi.data.database.tables.HistoryTable.TABLE
class HistoryTypeMapping : SQLiteTypeMapping<History>( class HistoryTypeMapping : SQLiteTypeMapping<History>(
HistoryPutResolver(), HistoryPutResolver(),
HistoryGetResolver(), HistoryGetResolver(),
HistoryDeleteResolver() HistoryDeleteResolver()
) )
open class HistoryPutResolver : DefaultPutResolver<History>() { open class HistoryPutResolver : DefaultPutResolver<History>() {
override fun mapToInsertQuery(obj: History) = InsertQuery.builder() override fun mapToInsertQuery(obj: History) = InsertQuery.builder()
.table(TABLE) .table(TABLE)
.build() .build()
override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder() override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder()
.table(TABLE) .table(TABLE)
.where("$COL_ID = ?") .where("$COL_ID = ?")
.whereArgs(obj.id) .whereArgs(obj.id)
.build() .build()
override fun mapToContentValues(obj: History) = ContentValues(4).apply { override fun mapToContentValues(obj: History) = ContentValues(4).apply {
put(COL_ID, obj.id) put(COL_ID, obj.id)
@ -56,8 +56,8 @@ class HistoryGetResolver : DefaultGetResolver<History>() {
class HistoryDeleteResolver : DefaultDeleteResolver<History>() { class HistoryDeleteResolver : DefaultDeleteResolver<History>() {
override fun mapToDeleteQuery(obj: History) = DeleteQuery.builder() override fun mapToDeleteQuery(obj: History) = DeleteQuery.builder()
.table(TABLE) .table(TABLE)
.where("$COL_ID = ?") .where("$COL_ID = ?")
.whereArgs(obj.id) .whereArgs(obj.id)
.build() .build()
} }

View File

@ -16,22 +16,22 @@ import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.COL_MANGA_ID
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.TABLE import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.TABLE
class MangaCategoryTypeMapping : SQLiteTypeMapping<MangaCategory>( class MangaCategoryTypeMapping : SQLiteTypeMapping<MangaCategory>(
MangaCategoryPutResolver(), MangaCategoryPutResolver(),
MangaCategoryGetResolver(), MangaCategoryGetResolver(),
MangaCategoryDeleteResolver() MangaCategoryDeleteResolver()
) )
class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() { class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() {
override fun mapToInsertQuery(obj: MangaCategory) = InsertQuery.builder() override fun mapToInsertQuery(obj: MangaCategory) = InsertQuery.builder()
.table(TABLE) .table(TABLE)
.build() .build()
override fun mapToUpdateQuery(obj: MangaCategory) = UpdateQuery.builder() override fun mapToUpdateQuery(obj: MangaCategory) = UpdateQuery.builder()
.table(TABLE) .table(TABLE)
.where("$COL_ID = ?") .where("$COL_ID = ?")
.whereArgs(obj.id) .whereArgs(obj.id)
.build() .build()
override fun mapToContentValues(obj: MangaCategory) = ContentValues(3).apply { override fun mapToContentValues(obj: MangaCategory) = ContentValues(3).apply {
put(COL_ID, obj.id) put(COL_ID, obj.id)
@ -52,8 +52,8 @@ class MangaCategoryGetResolver : DefaultGetResolver<MangaCategory>() {
class MangaCategoryDeleteResolver : DefaultDeleteResolver<MangaCategory>() { class MangaCategoryDeleteResolver : DefaultDeleteResolver<MangaCategory>() {
override fun mapToDeleteQuery(obj: MangaCategory) = DeleteQuery.builder() override fun mapToDeleteQuery(obj: MangaCategory) = DeleteQuery.builder()
.table(TABLE) .table(TABLE)
.where("$COL_ID = ?") .where("$COL_ID = ?")
.whereArgs(obj.id) .whereArgs(obj.id)
.build() .build()
} }

View File

@ -29,22 +29,22 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_VIEWER
import eu.kanade.tachiyomi.data.database.tables.MangaTable.TABLE import eu.kanade.tachiyomi.data.database.tables.MangaTable.TABLE
class MangaTypeMapping : SQLiteTypeMapping<Manga>( class MangaTypeMapping : SQLiteTypeMapping<Manga>(
MangaPutResolver(), MangaPutResolver(),
MangaGetResolver(), MangaGetResolver(),
MangaDeleteResolver() MangaDeleteResolver()
) )
class MangaPutResolver : DefaultPutResolver<Manga>() { class MangaPutResolver : DefaultPutResolver<Manga>() {
override fun mapToInsertQuery(obj: Manga) = InsertQuery.builder() override fun mapToInsertQuery(obj: Manga) = InsertQuery.builder()
.table(TABLE) .table(TABLE)
.build() .build()
override fun mapToUpdateQuery(obj: Manga) = UpdateQuery.builder() override fun mapToUpdateQuery(obj: Manga) = UpdateQuery.builder()
.table(TABLE) .table(TABLE)
.where("$COL_ID = ?") .where("$COL_ID = ?")
.whereArgs(obj.id) .whereArgs(obj.id)
.build() .build()
override fun mapToContentValues(obj: Manga) = ContentValues(15).apply { override fun mapToContentValues(obj: Manga) = ContentValues(15).apply {
put(COL_ID, obj.id) put(COL_ID, obj.id)
@ -95,8 +95,8 @@ open class MangaGetResolver : DefaultGetResolver<Manga>(), BaseMangaGetResolver
class MangaDeleteResolver : DefaultDeleteResolver<Manga>() { class MangaDeleteResolver : DefaultDeleteResolver<Manga>() {
override fun mapToDeleteQuery(obj: Manga) = DeleteQuery.builder() override fun mapToDeleteQuery(obj: Manga) = DeleteQuery.builder()
.table(TABLE) .table(TABLE)
.where("$COL_ID = ?") .where("$COL_ID = ?")
.whereArgs(obj.id) .whereArgs(obj.id)
.build() .build()
} }

View File

@ -27,22 +27,22 @@ import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TRACKING_URL
import eu.kanade.tachiyomi.data.database.tables.TrackTable.TABLE import eu.kanade.tachiyomi.data.database.tables.TrackTable.TABLE
class TrackTypeMapping : SQLiteTypeMapping<Track>( class TrackTypeMapping : SQLiteTypeMapping<Track>(
TrackPutResolver(), TrackPutResolver(),
TrackGetResolver(), TrackGetResolver(),
TrackDeleteResolver() TrackDeleteResolver()
) )
class TrackPutResolver : DefaultPutResolver<Track>() { class TrackPutResolver : DefaultPutResolver<Track>() {
override fun mapToInsertQuery(obj: Track) = InsertQuery.builder() override fun mapToInsertQuery(obj: Track) = InsertQuery.builder()
.table(TABLE) .table(TABLE)
.build() .build()
override fun mapToUpdateQuery(obj: Track) = UpdateQuery.builder() override fun mapToUpdateQuery(obj: Track) = UpdateQuery.builder()
.table(TABLE) .table(TABLE)
.where("$COL_ID = ?") .where("$COL_ID = ?")
.whereArgs(obj.id) .whereArgs(obj.id)
.build() .build()
override fun mapToContentValues(obj: Track) = ContentValues(10).apply { override fun mapToContentValues(obj: Track) = ContentValues(10).apply {
put(COL_ID, obj.id) put(COL_ID, obj.id)
@ -83,8 +83,8 @@ class TrackGetResolver : DefaultGetResolver<Track>() {
class TrackDeleteResolver : DefaultDeleteResolver<Track>() { class TrackDeleteResolver : DefaultDeleteResolver<Track>() {
override fun mapToDeleteQuery(obj: Track) = DeleteQuery.builder() override fun mapToDeleteQuery(obj: Track) = DeleteQuery.builder()
.table(TABLE) .table(TABLE)
.where("$COL_ID = ?") .where("$COL_ID = ?")
.whereArgs(obj.id) .whereArgs(obj.id)
.build() .build()
} }

View File

@ -10,20 +10,24 @@ import eu.kanade.tachiyomi.data.database.tables.CategoryTable
interface CategoryQueries : DbProvider { interface CategoryQueries : DbProvider {
fun getCategories() = db.get() fun getCategories() = db.get()
.listOfObjects(Category::class.java) .listOfObjects(Category::class.java)
.withQuery(Query.builder() .withQuery(
.table(CategoryTable.TABLE) Query.builder()
.orderBy(CategoryTable.COL_ORDER) .table(CategoryTable.TABLE)
.build()) .orderBy(CategoryTable.COL_ORDER)
.prepare() .build()
)
.prepare()
fun getCategoriesForManga(manga: Manga) = db.get() fun getCategoriesForManga(manga: Manga) = db.get()
.listOfObjects(Category::class.java) .listOfObjects(Category::class.java)
.withQuery(RawQuery.builder() .withQuery(
.query(getCategoriesForMangaQuery()) RawQuery.builder()
.args(manga.id) .query(getCategoriesForMangaQuery())
.build()) .args(manga.id)
.prepare() .build()
)
.prepare()
fun insertCategory(category: Category) = db.put().`object`(category).prepare() fun insertCategory(category: Category) = db.put().`object`(category).prepare()

View File

@ -16,50 +16,60 @@ import java.util.Date
interface ChapterQueries : DbProvider { interface ChapterQueries : DbProvider {
fun getChapters(manga: Manga) = db.get() fun getChapters(manga: Manga) = db.get()
.listOfObjects(Chapter::class.java) .listOfObjects(Chapter::class.java)
.withQuery(Query.builder() .withQuery(
.table(ChapterTable.TABLE) Query.builder()
.where("${ChapterTable.COL_MANGA_ID} = ?") .table(ChapterTable.TABLE)
.whereArgs(manga.id) .where("${ChapterTable.COL_MANGA_ID} = ?")
.build()) .whereArgs(manga.id)
.prepare() .build()
)
.prepare()
fun getRecentChapters(date: Date) = db.get() fun getRecentChapters(date: Date) = db.get()
.listOfObjects(MangaChapter::class.java) .listOfObjects(MangaChapter::class.java)
.withQuery(RawQuery.builder() .withQuery(
.query(getRecentsQuery()) RawQuery.builder()
.args(date.time) .query(getRecentsQuery())
.observesTables(ChapterTable.TABLE) .args(date.time)
.build()) .observesTables(ChapterTable.TABLE)
.withGetResolver(MangaChapterGetResolver.INSTANCE) .build()
.prepare() )
.withGetResolver(MangaChapterGetResolver.INSTANCE)
.prepare()
fun getChapter(id: Long) = db.get() fun getChapter(id: Long) = db.get()
.`object`(Chapter::class.java) .`object`(Chapter::class.java)
.withQuery(Query.builder() .withQuery(
.table(ChapterTable.TABLE) Query.builder()
.where("${ChapterTable.COL_ID} = ?") .table(ChapterTable.TABLE)
.whereArgs(id) .where("${ChapterTable.COL_ID} = ?")
.build()) .whereArgs(id)
.prepare() .build()
)
.prepare()
fun getChapter(url: String) = db.get() fun getChapter(url: String) = db.get()
.`object`(Chapter::class.java) .`object`(Chapter::class.java)
.withQuery(Query.builder() .withQuery(
.table(ChapterTable.TABLE) Query.builder()
.where("${ChapterTable.COL_URL} = ?") .table(ChapterTable.TABLE)
.whereArgs(url) .where("${ChapterTable.COL_URL} = ?")
.build()) .whereArgs(url)
.prepare() .build()
)
.prepare()
fun getChapter(url: String, mangaId: Long) = db.get() fun getChapter(url: String, mangaId: Long) = db.get()
.`object`(Chapter::class.java) .`object`(Chapter::class.java)
.withQuery(Query.builder() .withQuery(
.table(ChapterTable.TABLE) Query.builder()
.where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?") .table(ChapterTable.TABLE)
.whereArgs(url, mangaId) .where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?")
.build()) .whereArgs(url, mangaId)
.prepare() .build()
)
.prepare()
fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).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 deleteChapters(chapters: List<Chapter>) = db.delete().objects(chapters).prepare()
fun updateChaptersBackup(chapters: List<Chapter>) = db.put() fun updateChaptersBackup(chapters: List<Chapter>) = db.put()
.objects(chapters) .objects(chapters)
.withPutResolver(ChapterBackupPutResolver()) .withPutResolver(ChapterBackupPutResolver())
.prepare() .prepare()
fun updateChapterProgress(chapter: Chapter) = db.put() fun updateChapterProgress(chapter: Chapter) = db.put()
.`object`(chapter) .`object`(chapter)
.withPutResolver(ChapterProgressPutResolver()) .withPutResolver(ChapterProgressPutResolver())
.prepare() .prepare()
fun updateChaptersProgress(chapters: List<Chapter>) = db.put() fun updateChaptersProgress(chapters: List<Chapter>) = db.put()
.objects(chapters) .objects(chapters)
.withPutResolver(ChapterProgressPutResolver()) .withPutResolver(ChapterProgressPutResolver())
.prepare() .prepare()
fun fixChaptersSourceOrder(chapters: List<Chapter>) = db.put() fun fixChaptersSourceOrder(chapters: List<Chapter>) = db.put()
.objects(chapters) .objects(chapters)
.withPutResolver(ChapterSourceOrderPutResolver()) .withPutResolver(ChapterSourceOrderPutResolver())
.prepare() .prepare()
} }

View File

@ -23,32 +23,38 @@ interface HistoryQueries : DbProvider {
* @param date recent date range * @param date recent date range
*/ */
fun getRecentManga(date: Date) = db.get() fun getRecentManga(date: Date) = db.get()
.listOfObjects(MangaChapterHistory::class.java) .listOfObjects(MangaChapterHistory::class.java)
.withQuery(RawQuery.builder() .withQuery(
.query(getRecentMangasQuery()) RawQuery.builder()
.args(date.time) .query(getRecentMangasQuery())
.observesTables(HistoryTable.TABLE) .args(date.time)
.build()) .observesTables(HistoryTable.TABLE)
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE) .build()
.prepare() )
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
.prepare()
fun getHistoryByMangaId(mangaId: Long) = db.get() fun getHistoryByMangaId(mangaId: Long) = db.get()
.listOfObjects(History::class.java) .listOfObjects(History::class.java)
.withQuery(RawQuery.builder() .withQuery(
.query(getHistoryByMangaId()) RawQuery.builder()
.args(mangaId) .query(getHistoryByMangaId())
.observesTables(HistoryTable.TABLE) .args(mangaId)
.build()) .observesTables(HistoryTable.TABLE)
.prepare() .build()
)
.prepare()
fun getHistoryByChapterUrl(chapterUrl: String) = db.get() fun getHistoryByChapterUrl(chapterUrl: String) = db.get()
.`object`(History::class.java) .`object`(History::class.java)
.withQuery(RawQuery.builder() .withQuery(
.query(getHistoryByChapterUrl()) RawQuery.builder()
.args(chapterUrl) .query(getHistoryByChapterUrl())
.observesTables(HistoryTable.TABLE) .args(chapterUrl)
.build()) .observesTables(HistoryTable.TABLE)
.prepare() .build()
)
.prepare()
/** /**
* Updates the history last read. * Updates the history last read.
@ -56,9 +62,9 @@ interface HistoryQueries : DbProvider {
* @param history history object * @param history history object
*/ */
fun updateHistoryLastRead(history: History) = db.put() fun updateHistoryLastRead(history: History) = db.put()
.`object`(history) .`object`(history)
.withPutResolver(HistoryLastReadPutResolver()) .withPutResolver(HistoryLastReadPutResolver())
.prepare() .prepare()
/** /**
* Updates the history last read. * Updates the history last read.
@ -66,21 +72,25 @@ interface HistoryQueries : DbProvider {
* @param historyList history object list * @param historyList history object list
*/ */
fun updateHistoryLastRead(historyList: List<History>) = db.put() fun updateHistoryLastRead(historyList: List<History>) = db.put()
.objects(historyList) .objects(historyList)
.withPutResolver(HistoryLastReadPutResolver()) .withPutResolver(HistoryLastReadPutResolver())
.prepare() .prepare()
fun deleteHistory() = db.delete() fun deleteHistory() = db.delete()
.byQuery(DeleteQuery.builder() .byQuery(
.table(HistoryTable.TABLE) DeleteQuery.builder()
.build()) .table(HistoryTable.TABLE)
.prepare() .build()
)
.prepare()
fun deleteHistoryNoLastRead() = db.delete() fun deleteHistoryNoLastRead() = db.delete()
.byQuery(DeleteQuery.builder() .byQuery(
.table(HistoryTable.TABLE) DeleteQuery.builder()
.where("${HistoryTable.COL_LAST_READ} = ?") .table(HistoryTable.TABLE)
.whereArgs(0) .where("${HistoryTable.COL_LAST_READ} = ?")
.build()) .whereArgs(0)
.prepare() .build()
)
.prepare()
} }

View File

@ -15,12 +15,14 @@ interface MangaCategoryQueries : DbProvider {
fun insertMangasCategories(mangasCategories: List<MangaCategory>) = db.put().objects(mangasCategories).prepare() fun insertMangasCategories(mangasCategories: List<MangaCategory>) = db.put().objects(mangasCategories).prepare()
fun deleteOldMangasCategories(mangas: List<Manga>) = db.delete() fun deleteOldMangasCategories(mangas: List<Manga>) = db.delete()
.byQuery(DeleteQuery.builder() .byQuery(
.table(MangaCategoryTable.TABLE) DeleteQuery.builder()
.where("${MangaCategoryTable.COL_MANGA_ID} IN (${Queries.placeholders(mangas.size)})") .table(MangaCategoryTable.TABLE)
.whereArgs(*mangas.map { it.id }.toTypedArray()) .where("${MangaCategoryTable.COL_MANGA_ID} IN (${Queries.placeholders(mangas.size)})")
.build()) .whereArgs(*mangas.map { it.id }.toTypedArray())
.prepare() .build()
)
.prepare()
fun setMangaCategories(mangasCategories: List<MangaCategory>, mangas: List<Manga>) { fun setMangaCategories(mangasCategories: List<MangaCategory>, mangas: List<Manga>) {
db.inTransaction { db.inTransaction {

View File

@ -20,117 +20,137 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable
interface MangaQueries : DbProvider { interface MangaQueries : DbProvider {
fun getMangas() = db.get() fun getMangas() = db.get()
.listOfObjects(Manga::class.java) .listOfObjects(Manga::class.java)
.withQuery(Query.builder() .withQuery(
.table(MangaTable.TABLE) Query.builder()
.build()) .table(MangaTable.TABLE)
.prepare() .build()
)
.prepare()
fun getLibraryMangas() = db.get() fun getLibraryMangas() = db.get()
.listOfObjects(LibraryManga::class.java) .listOfObjects(LibraryManga::class.java)
.withQuery(RawQuery.builder() .withQuery(
.query(libraryQuery) RawQuery.builder()
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE) .query(libraryQuery)
.build()) .observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE)
.withGetResolver(LibraryMangaGetResolver.INSTANCE) .build()
.prepare() )
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
.prepare()
fun getFavoriteMangas() = db.get() fun getFavoriteMangas() = db.get()
.listOfObjects(Manga::class.java) .listOfObjects(Manga::class.java)
.withQuery(Query.builder() .withQuery(
.table(MangaTable.TABLE) Query.builder()
.where("${MangaTable.COL_FAVORITE} = ?") .table(MangaTable.TABLE)
.whereArgs(1) .where("${MangaTable.COL_FAVORITE} = ?")
.orderBy(MangaTable.COL_TITLE) .whereArgs(1)
.build()) .orderBy(MangaTable.COL_TITLE)
.prepare() .build()
)
.prepare()
fun getManga(url: String, sourceId: Long) = db.get() fun getManga(url: String, sourceId: Long) = db.get()
.`object`(Manga::class.java) .`object`(Manga::class.java)
.withQuery(Query.builder() .withQuery(
.table(MangaTable.TABLE) Query.builder()
.where("${MangaTable.COL_URL} = ? AND ${MangaTable.COL_SOURCE} = ?") .table(MangaTable.TABLE)
.whereArgs(url, sourceId) .where("${MangaTable.COL_URL} = ? AND ${MangaTable.COL_SOURCE} = ?")
.build()) .whereArgs(url, sourceId)
.prepare() .build()
)
.prepare()
fun getManga(id: Long) = db.get() fun getManga(id: Long) = db.get()
.`object`(Manga::class.java) .`object`(Manga::class.java)
.withQuery(Query.builder() .withQuery(
.table(MangaTable.TABLE) Query.builder()
.where("${MangaTable.COL_ID} = ?") .table(MangaTable.TABLE)
.whereArgs(id) .where("${MangaTable.COL_ID} = ?")
.build()) .whereArgs(id)
.prepare() .build()
)
.prepare()
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare() fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare() fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare()
fun updateFlags(manga: Manga) = db.put() fun updateFlags(manga: Manga) = db.put()
.`object`(manga) .`object`(manga)
.withPutResolver(MangaFlagsPutResolver()) .withPutResolver(MangaFlagsPutResolver())
.prepare() .prepare()
fun updateLastUpdated(manga: Manga) = db.put() fun updateLastUpdated(manga: Manga) = db.put()
.`object`(manga) .`object`(manga)
.withPutResolver(MangaLastUpdatedPutResolver()) .withPutResolver(MangaLastUpdatedPutResolver())
.prepare() .prepare()
fun updateMangaFavorite(manga: Manga) = db.put() fun updateMangaFavorite(manga: Manga) = db.put()
.`object`(manga) .`object`(manga)
.withPutResolver(MangaFavoritePutResolver()) .withPutResolver(MangaFavoritePutResolver())
.prepare() .prepare()
fun updateMangaViewer(manga: Manga) = db.put() fun updateMangaViewer(manga: Manga) = db.put()
.`object`(manga) .`object`(manga)
.withPutResolver(MangaViewerPutResolver()) .withPutResolver(MangaViewerPutResolver())
.prepare() .prepare()
fun updateMangaTitle(manga: Manga) = db.put() fun updateMangaTitle(manga: Manga) = db.put()
.`object`(manga) .`object`(manga)
.withPutResolver(MangaTitlePutResolver()) .withPutResolver(MangaTitlePutResolver())
.prepare() .prepare()
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare() fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare() fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
fun deleteMangasNotInLibrary() = db.delete() fun deleteMangasNotInLibrary() = db.delete()
.byQuery(DeleteQuery.builder() .byQuery(
.table(MangaTable.TABLE) DeleteQuery.builder()
.where("${MangaTable.COL_FAVORITE} = ?") .table(MangaTable.TABLE)
.whereArgs(0) .where("${MangaTable.COL_FAVORITE} = ?")
.build()) .whereArgs(0)
.prepare() .build()
)
.prepare()
fun deleteMangas() = db.delete() fun deleteMangas() = db.delete()
.byQuery(DeleteQuery.builder() .byQuery(
.table(MangaTable.TABLE) DeleteQuery.builder()
.build()) .table(MangaTable.TABLE)
.prepare() .build()
)
.prepare()
fun getLastReadManga() = db.get() fun getLastReadManga() = db.get()
.listOfObjects(Manga::class.java) .listOfObjects(Manga::class.java)
.withQuery(RawQuery.builder() .withQuery(
.query(getLastReadMangaQuery()) RawQuery.builder()
.observesTables(MangaTable.TABLE) .query(getLastReadMangaQuery())
.build()) .observesTables(MangaTable.TABLE)
.prepare() .build()
)
.prepare()
fun getTotalChapterManga() = db.get() fun getTotalChapterManga() = db.get()
.listOfObjects(Manga::class.java) .listOfObjects(Manga::class.java)
.withQuery(RawQuery.builder() .withQuery(
.query(getTotalChapterMangaQuery()) RawQuery.builder()
.observesTables(MangaTable.TABLE) .query(getTotalChapterMangaQuery())
.build()) .observesTables(MangaTable.TABLE)
.prepare() .build()
)
.prepare()
fun getLatestChapterManga() = db.get() fun getLatestChapterManga() = db.get()
.listOfObjects(Manga::class.java) .listOfObjects(Manga::class.java)
.withQuery(RawQuery.builder() .withQuery(
.query(getLatestChapterMangaQuery()) RawQuery.builder()
.observesTables(MangaTable.TABLE) .query(getLatestChapterMangaQuery())
.build()) .observesTables(MangaTable.TABLE)
.prepare() .build()
)
.prepare()
} }

View File

@ -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. * 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} SELECT M.*, COALESCE(MC.${MangaCategory.COL_CATEGORY_ID}, 0) AS ${Manga.COL_CATEGORY}
FROM ( FROM (
SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD} 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. * 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} 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} ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
WHERE ${Manga.COL_FAVORITE} = 1 AND ${Chapter.COL_DATE_UPLOAD} > ? WHERE ${Manga.COL_FAVORITE} = 1 AND ${Chapter.COL_DATE_UPLOAD} > ?
@ -47,7 +49,8 @@ fun getRecentsQuery() = """
* and are read after the given time period * and are read after the given time period
* @return return limit is 25 * @return return limit is 25
*/ */
fun getRecentMangasQuery() = """ fun getRecentMangasQuery() =
"""
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, ${Manga.TABLE}.*, ${Chapter.TABLE}.*, ${History.TABLE}.* SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, ${Manga.TABLE}.*, ${Chapter.TABLE}.*, ${History.TABLE}.*
FROM ${Manga.TABLE} FROM ${Manga.TABLE}
JOIN ${Chapter.TABLE} JOIN ${Chapter.TABLE}
@ -65,7 +68,8 @@ fun getRecentMangasQuery() = """
LIMIT 25 LIMIT 25
""" """
fun getHistoryByMangaId() = """ fun getHistoryByMangaId() =
"""
SELECT ${History.TABLE}.* SELECT ${History.TABLE}.*
FROM ${History.TABLE} FROM ${History.TABLE}
JOIN ${Chapter.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} 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}.* SELECT ${History.TABLE}.*
FROM ${History.TABLE} FROM ${History.TABLE}
JOIN ${Chapter.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} 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 SELECT ${Manga.TABLE}.*, MAX(${History.TABLE}.${History.COL_LAST_READ}) AS max
FROM ${Manga.TABLE} FROM ${Manga.TABLE}
JOIN ${Chapter.TABLE} JOIN ${Chapter.TABLE}
@ -93,7 +99,8 @@ fun getLastReadMangaQuery() = """
ORDER BY max DESC ORDER BY max DESC
""" """
fun getTotalChapterMangaQuery() = """ fun getTotalChapterMangaQuery() =
"""
SELECT ${Manga.TABLE}.* SELECT ${Manga.TABLE}.*
FROM ${Manga.TABLE} FROM ${Manga.TABLE}
JOIN ${Chapter.TABLE} JOIN ${Chapter.TABLE}
@ -102,7 +109,8 @@ fun getTotalChapterMangaQuery() = """
ORDER by COUNT(*) ORDER by COUNT(*)
""" """
fun getLatestChapterMangaQuery() = """ fun getLatestChapterMangaQuery() =
"""
SELECT ${Manga.TABLE}.*, MAX(${Chapter.TABLE}.${Chapter.COL_DATE_UPLOAD}) AS max SELECT ${Manga.TABLE}.*, MAX(${Chapter.TABLE}.${Chapter.COL_DATE_UPLOAD}) AS max
FROM ${Manga.TABLE} FROM ${Manga.TABLE}
JOIN ${Chapter.TABLE} JOIN ${Chapter.TABLE}
@ -114,7 +122,8 @@ fun getLatestChapterMangaQuery() = """
/** /**
* Query to get the categories for a manga. * Query to get the categories for a manga.
*/ */
fun getCategoriesForMangaQuery() = """ fun getCategoriesForMangaQuery() =
"""
SELECT ${Category.TABLE}.* FROM ${Category.TABLE} SELECT ${Category.TABLE}.* FROM ${Category.TABLE}
JOIN ${MangaCategory.TABLE} ON ${Category.TABLE}.${Category.COL_ID} = JOIN ${MangaCategory.TABLE} ON ${Category.TABLE}.${Category.COL_ID} =
${MangaCategory.TABLE}.${MangaCategory.COL_CATEGORY_ID} ${MangaCategory.TABLE}.${MangaCategory.COL_CATEGORY_ID}

View File

@ -11,23 +11,27 @@ import eu.kanade.tachiyomi.data.track.TrackService
interface TrackQueries : DbProvider { interface TrackQueries : DbProvider {
fun getTracks(manga: Manga) = db.get() fun getTracks(manga: Manga) = db.get()
.listOfObjects(Track::class.java) .listOfObjects(Track::class.java)
.withQuery(Query.builder() .withQuery(
.table(TrackTable.TABLE) Query.builder()
.where("${TrackTable.COL_MANGA_ID} = ?") .table(TrackTable.TABLE)
.whereArgs(manga.id) .where("${TrackTable.COL_MANGA_ID} = ?")
.build()) .whereArgs(manga.id)
.prepare() .build()
)
.prepare()
fun insertTrack(track: Track) = db.put().`object`(track).prepare() fun insertTrack(track: Track) = db.put().`object`(track).prepare()
fun insertTracks(tracks: List<Track>) = db.put().objects(tracks).prepare() fun insertTracks(tracks: List<Track>) = db.put().objects(tracks).prepare()
fun deleteTrackForManga(manga: Manga, sync: TrackService) = db.delete() fun deleteTrackForManga(manga: Manga, sync: TrackService) = db.delete()
.byQuery(DeleteQuery.builder() .byQuery(
.table(TrackTable.TABLE) DeleteQuery.builder()
.where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?") .table(TrackTable.TABLE)
.whereArgs(manga.id, sync.id) .where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?")
.build()) .whereArgs(manga.id, sync.id)
.prepare() .build()
)
.prepare()
} }

View File

@ -20,10 +20,10 @@ class ChapterBackupPutResolver : PutResolver<Chapter>() {
} }
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder() fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
.table(ChapterTable.TABLE) .table(ChapterTable.TABLE)
.where("${ChapterTable.COL_URL} = ?") .where("${ChapterTable.COL_URL} = ?")
.whereArgs(chapter.url) .whereArgs(chapter.url)
.build() .build()
fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply { fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply {
put(ChapterTable.COL_READ, chapter.read) put(ChapterTable.COL_READ, chapter.read)

View File

@ -20,10 +20,10 @@ class ChapterProgressPutResolver : PutResolver<Chapter>() {
} }
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder() fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
.table(ChapterTable.TABLE) .table(ChapterTable.TABLE)
.where("${ChapterTable.COL_ID} = ?") .where("${ChapterTable.COL_ID} = ?")
.whereArgs(chapter.id) .whereArgs(chapter.id)
.build() .build()
fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply { fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply {
put(ChapterTable.COL_READ, chapter.read) put(ChapterTable.COL_READ, chapter.read)

View File

@ -20,10 +20,10 @@ class ChapterSourceOrderPutResolver : PutResolver<Chapter>() {
} }
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder() fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
.table(ChapterTable.TABLE) .table(ChapterTable.TABLE)
.where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?") .where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?")
.whereArgs(chapter.url, chapter.manga_id) .whereArgs(chapter.url, chapter.manga_id)
.build() .build()
fun mapToContentValues(chapter: Chapter) = ContentValues(1).apply { fun mapToContentValues(chapter: Chapter) = ContentValues(1).apply {
put(ChapterTable.COL_SOURCE_ORDER, chapter.source_order) put(ChapterTable.COL_SOURCE_ORDER, chapter.source_order)

View File

@ -19,11 +19,13 @@ class HistoryLastReadPutResolver : HistoryPutResolver() {
override fun performPut(@NonNull db: StorIOSQLite, @NonNull history: History): PutResult = db.inTransactionReturn { override fun performPut(@NonNull db: StorIOSQLite, @NonNull history: History): PutResult = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(history) val updateQuery = mapToUpdateQuery(history)
val cursor = db.lowLevel().query(Query.builder() val cursor = db.lowLevel().query(
Query.builder()
.table(updateQuery.table()) .table(updateQuery.table())
.where(updateQuery.where()) .where(updateQuery.where())
.whereArgs(updateQuery.whereArgs()) .whereArgs(updateQuery.whereArgs())
.build()) .build()
)
val putResult: PutResult val putResult: PutResult
@ -46,10 +48,10 @@ class HistoryLastReadPutResolver : HistoryPutResolver() {
* @param obj history object * @param obj history object
*/ */
override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder() override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder()
.table(HistoryTable.TABLE) .table(HistoryTable.TABLE)
.where("${HistoryTable.COL_CHAPTER_ID} = ?") .where("${HistoryTable.COL_CHAPTER_ID} = ?")
.whereArgs(obj.chapter_id) .whereArgs(obj.chapter_id)
.build() .build()
/** /**
* Create content query * Create content query

View File

@ -20,10 +20,10 @@ class MangaFavoritePutResolver : PutResolver<Manga>() {
} }
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder() fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
.table(MangaTable.TABLE) .table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?") .where("${MangaTable.COL_ID} = ?")
.whereArgs(manga.id) .whereArgs(manga.id)
.build() .build()
fun mapToContentValues(manga: Manga) = ContentValues(1).apply { fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
put(MangaTable.COL_FAVORITE, manga.favorite) put(MangaTable.COL_FAVORITE, manga.favorite)

View File

@ -20,10 +20,10 @@ class MangaFlagsPutResolver : PutResolver<Manga>() {
} }
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder() fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
.table(MangaTable.TABLE) .table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?") .where("${MangaTable.COL_ID} = ?")
.whereArgs(manga.id) .whereArgs(manga.id)
.build() .build()
fun mapToContentValues(manga: Manga) = ContentValues(1).apply { fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
put(MangaTable.COL_CHAPTER_FLAGS, manga.chapter_flags) put(MangaTable.COL_CHAPTER_FLAGS, manga.chapter_flags)

View File

@ -20,10 +20,10 @@ class MangaLastUpdatedPutResolver : PutResolver<Manga>() {
} }
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder() fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
.table(MangaTable.TABLE) .table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?") .where("${MangaTable.COL_ID} = ?")
.whereArgs(manga.id) .whereArgs(manga.id)
.build() .build()
fun mapToContentValues(manga: Manga) = ContentValues(1).apply { fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
put(MangaTable.COL_LAST_UPDATE, manga.last_update) put(MangaTable.COL_LAST_UPDATE, manga.last_update)

View File

@ -20,10 +20,10 @@ class MangaTitlePutResolver : PutResolver<Manga>() {
} }
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder() fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
.table(MangaTable.TABLE) .table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?") .where("${MangaTable.COL_ID} = ?")
.whereArgs(manga.id) .whereArgs(manga.id)
.build() .build()
fun mapToContentValues(manga: Manga) = ContentValues(1).apply { fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
put(MangaTable.COL_TITLE, manga.title) put(MangaTable.COL_TITLE, manga.title)

View File

@ -20,10 +20,10 @@ class MangaViewerPutResolver : PutResolver<Manga>() {
} }
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder() fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
.table(MangaTable.TABLE) .table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?") .where("${MangaTable.COL_ID} = ?")
.whereArgs(manga.id) .whereArgs(manga.id)
.build() .build()
fun mapToContentValues(manga: Manga) = ContentValues(1).apply { fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
put(MangaTable.COL_VIEWER, manga.viewer) put(MangaTable.COL_VIEWER, manga.viewer)

View File

@ -13,7 +13,8 @@ object CategoryTable {
const val COL_FLAGS = "flags" const val COL_FLAGS = "flags"
val createTableQuery: String val createTableQuery: String
get() = """CREATE TABLE $TABLE( get() =
"""CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY, $COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_NAME TEXT NOT NULL, $COL_NAME TEXT NOT NULL,
$COL_ORDER INTEGER NOT NULL, $COL_ORDER INTEGER NOT NULL,

View File

@ -29,7 +29,8 @@ object ChapterTable {
const val COL_SOURCE_ORDER = "source_order" const val COL_SOURCE_ORDER = "source_order"
val createTableQuery: String val createTableQuery: String
get() = """CREATE TABLE $TABLE( get() =
"""CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY, $COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_MANGA_ID INTEGER NOT NULL, $COL_MANGA_ID INTEGER NOT NULL,
$COL_URL TEXT NOT NULL, $COL_URL TEXT NOT NULL,
@ -51,7 +52,7 @@ object ChapterTable {
val createUnreadChaptersIndexQuery: String val createUnreadChaptersIndexQuery: String
get() = "CREATE INDEX ${TABLE}_unread_by_manga_index ON $TABLE($COL_MANGA_ID, $COL_READ) " + 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 val sourceOrderUpdateQuery: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SOURCE_ORDER INTEGER DEFAULT 0" get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SOURCE_ORDER INTEGER DEFAULT 0"

View File

@ -31,7 +31,8 @@ object HistoryTable {
* query to create history table * query to create history table
*/ */
val createTableQuery: String val createTableQuery: String
get() = """CREATE TABLE $TABLE( get() =
"""CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY, $COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_CHAPTER_ID INTEGER NOT NULL UNIQUE, $COL_CHAPTER_ID INTEGER NOT NULL UNIQUE,
$COL_LAST_READ LONG, $COL_LAST_READ LONG,

View File

@ -11,7 +11,8 @@ object MangaCategoryTable {
const val COL_CATEGORY_ID = "category_id" const val COL_CATEGORY_ID = "category_id"
val createTableQuery: String val createTableQuery: String
get() = """CREATE TABLE $TABLE( get() =
"""CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY, $COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_MANGA_ID INTEGER NOT NULL, $COL_MANGA_ID INTEGER NOT NULL,
$COL_CATEGORY_ID INTEGER NOT NULL, $COL_CATEGORY_ID INTEGER NOT NULL,

View File

@ -39,7 +39,8 @@ object MangaTable {
const val COL_CATEGORY = "category" const val COL_CATEGORY = "category"
val createTableQuery: String val createTableQuery: String
get() = """CREATE TABLE $TABLE( get() =
"""CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY, $COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_SOURCE INTEGER NOT NULL, $COL_SOURCE INTEGER NOT NULL,
$COL_URL TEXT NOT NULL, $COL_URL TEXT NOT NULL,
@ -62,5 +63,5 @@ object MangaTable {
val createLibraryIndexQuery: String val createLibraryIndexQuery: String
get() = "CREATE INDEX library_${COL_FAVORITE}_index ON $TABLE($COL_FAVORITE) " + get() = "CREATE INDEX library_${COL_FAVORITE}_index ON $TABLE($COL_FAVORITE) " +
"WHERE $COL_FAVORITE = 1" "WHERE $COL_FAVORITE = 1"
} }

View File

@ -31,7 +31,8 @@ object TrackTable {
const val COL_FINISH_DATE = "finish_date" const val COL_FINISH_DATE = "finish_date"
val createTableQuery: String val createTableQuery: String
get() = """CREATE TABLE $TABLE( get() =
"""CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY, $COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_MANGA_ID INTEGER NOT NULL, $COL_MANGA_ID INTEGER NOT NULL,
$COL_SYNC_ID INTEGER NOT NULL, $COL_SYNC_ID INTEGER NOT NULL,

View File

@ -100,8 +100,8 @@ class DownloadCache(
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] val mangaDir = sourceDir.files[provider.getMangaDirName(manga)]
if (mangaDir != null) { if (mangaDir != null) {
return mangaDir.files return mangaDir.files
.filter { !it.endsWith(Downloader.TMP_DIR_SUFFIX) } .filter { !it.endsWith(Downloader.TMP_DIR_SUFFIX) }
.size .size
} }
} }
return 0 return 0
@ -125,26 +125,26 @@ class DownloadCache(
val onlineSources = sourceManager.getOnlineSources() val onlineSources = sourceManager.getOnlineSources()
val sourceDirs = rootDir.dir.listFiles() val sourceDirs = rootDir.dir.listFiles()
.orEmpty() .orEmpty()
.associate { it.name to SourceDirectory(it) } .associate { it.name to SourceDirectory(it) }
.mapNotNullKeys { entry -> .mapNotNullKeys { entry ->
onlineSources.find { provider.getSourceDirName(it) == entry.key }?.id onlineSources.find { provider.getSourceDirName(it) == entry.key }?.id
} }
rootDir.files = sourceDirs rootDir.files = sourceDirs
sourceDirs.values.forEach { sourceDir -> sourceDirs.values.forEach { sourceDir ->
val mangaDirs = sourceDir.dir.listFiles() val mangaDirs = sourceDir.dir.listFiles()
.orEmpty() .orEmpty()
.associateNotNullKeys { it.name to MangaDirectory(it) } .associateNotNullKeys { it.name to MangaDirectory(it) }
sourceDir.files = mangaDirs sourceDir.files = mangaDirs
mangaDirs.values.forEach { mangaDir -> mangaDirs.values.forEach { mangaDir ->
val chapterDirs = mangaDir.dir.listFiles() val chapterDirs = mangaDir.dir.listFiles()
.orEmpty() .orEmpty()
.mapNotNull { it.name } .mapNotNull { it.name }
.toHashSet() .toHashSet()
mangaDir.files = chapterDirs mangaDir.files = chapterDirs
} }

View File

@ -148,16 +148,16 @@ class DownloadManager(private val context: Context) {
private fun buildPageList(chapterDir: UniFile?): Observable<List<Page>> { private fun buildPageList(chapterDir: UniFile?): Observable<List<Page>> {
return Observable.fromCallable { return Observable.fromCallable {
val files = chapterDir?.listFiles().orEmpty() val files = chapterDir?.listFiles().orEmpty()
.filter { "image" in it.type.orEmpty() } .filter { "image" in it.type.orEmpty() }
if (files.isEmpty()) { if (files.isEmpty()) {
throw Exception("Page list is empty") throw Exception("Page list is empty")
} }
files.sortedBy { it.name } files.sortedBy { it.name }
.mapIndexed { i, file -> .mapIndexed { i, file ->
Page(i, uri = file.uri).apply { status = Page.READY } Page(i, uri = file.uri).apply { status = Page.READY }
} }
} }
} }

View File

@ -87,13 +87,15 @@ internal class DownloadNotifier(private val context: Context) {
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
isDownloading = true isDownloading = true
// Pause action // Pause action
addAction(R.drawable.ic_pause_24dp, addAction(
context.getString(R.string.action_pause), R.drawable.ic_pause_24dp,
NotificationReceiver.pauseDownloadsPendingBroadcast(context)) context.getString(R.string.action_pause),
NotificationReceiver.pauseDownloadsPendingBroadcast(context)
)
} }
val downloadingProgressText = context.getString(R.string.chapter_downloading_progress) val downloadingProgressText = context.getString(R.string.chapter_downloading_progress)
.format(download.downloadedImages, download.pages!!.size) .format(download.downloadedImages, download.pages!!.size)
if (preferences.hideNotificationContent()) { if (preferences.hideNotificationContent()) {
setContentTitle(downloadingProgressText) setContentTitle(downloadingProgressText)
@ -126,13 +128,17 @@ internal class DownloadNotifier(private val context: Context) {
// Open download manager when clicked // Open download manager when clicked
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
// Resume action // Resume action
addAction(R.drawable.ic_play_arrow_24dp, addAction(
context.getString(R.string.action_resume), R.drawable.ic_play_arrow_24dp,
NotificationReceiver.resumeDownloadsPendingBroadcast(context)) context.getString(R.string.action_resume),
NotificationReceiver.resumeDownloadsPendingBroadcast(context)
)
// Clear action // Clear action
addAction(R.drawable.ic_close_24dp, addAction(
context.getString(R.string.action_cancel_all), R.drawable.ic_close_24dp,
NotificationReceiver.clearDownloadsPendingBroadcast(context)) context.getString(R.string.action_cancel_all),
NotificationReceiver.clearDownloadsPendingBroadcast(context)
)
} }
// Show notification. // Show notification.
@ -173,8 +179,10 @@ internal class DownloadNotifier(private val context: Context) {
fun onError(error: String? = null, chapter: String? = null) { fun onError(error: String? = null, chapter: String? = null) {
// Create notification // Create notification
with(notificationBuilder) { with(notificationBuilder) {
setContentTitle(chapter setContentTitle(
?: context.getString(R.string.download_notifier_downloader_title)) chapter
?: context.getString(R.string.download_notifier_downloader_title)
)
setContentText(error ?: context.getString(R.string.download_notifier_unknown_error)) setContentText(error ?: context.getString(R.string.download_notifier_unknown_error))
setSmallIcon(android.R.drawable.stat_sys_warning) setSmallIcon(android.R.drawable.stat_sys_warning)
clearActions() clearActions()

View File

@ -52,8 +52,8 @@ class DownloadProvider(private val context: Context) {
internal fun getMangaDir(manga: Manga, source: Source): UniFile { internal fun getMangaDir(manga: Manga, source: Source): UniFile {
try { try {
return downloadsDir return downloadsDir
.createDirectory(getSourceDirName(source)) .createDirectory(getSourceDirName(source))
.createDirectory(getMangaDirName(manga)) .createDirectory(getMangaDirName(manga))
} catch (e: NullPointerException) { } catch (e: NullPointerException) {
throw Exception(context.getString(R.string.invalid_download_dir)) throw Exception(context.getString(R.string.invalid_download_dir))
} }

View File

@ -123,14 +123,17 @@ class DownloadService : Service() {
*/ */
private fun listenNetworkChanges() { private fun listenNetworkChanges() {
subscriptions += ReactiveNetwork.observeNetworkConnectivity(applicationContext) subscriptions += ReactiveNetwork.observeNetworkConnectivity(applicationContext)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe({ state -> .subscribe(
{ state ->
onNetworkStateChanged(state) onNetworkStateChanged(state)
}, { },
{
toast(R.string.download_queue_error) toast(R.string.download_queue_error)
stopSelf() stopSelf()
}) }
)
} }
/** /**
@ -162,10 +165,11 @@ class DownloadService : Service() {
*/ */
private fun listenDownloaderState() { private fun listenDownloaderState() {
subscriptions += downloadManager.runningRelay.subscribe { running -> subscriptions += downloadManager.runningRelay.subscribe { running ->
if (running) if (running) {
wakeLock.acquireIfNeeded() wakeLock.acquireIfNeeded()
else } else {
wakeLock.releaseIfNeeded() wakeLock.releaseIfNeeded()
}
} }
} }

View File

@ -77,9 +77,9 @@ class DownloadStore(
*/ */
fun restore(): List<Download> { fun restore(): List<Download> {
val objs = preferences.all val objs = preferences.all
.mapNotNull { it.value as? String } .mapNotNull { it.value as? String }
.mapNotNull { deserialize(it) } .mapNotNull { deserialize(it) }
.sortedBy { it.order } .sortedBy { it.order }
val downloads = mutableListOf<Download>() val downloads = mutableListOf<Download>()
if (objs.isNotEmpty()) { if (objs.isNotEmpty()) {

View File

@ -100,11 +100,13 @@ class Downloader(
* @return true if the downloader is started, false otherwise. * @return true if the downloader is started, false otherwise.
*/ */
fun start(): Boolean { fun start(): Boolean {
if (isRunning || queue.isEmpty()) if (isRunning || queue.isEmpty()) {
return false return false
}
if (!subscriptions.hasSubscriptions()) if (!subscriptions.hasSubscriptions()) {
initializeSubscriptions() initializeSubscriptions()
}
val pending = queue.filter { it.status != Download.DOWNLOADED } val pending = queue.filter { it.status != Download.DOWNLOADED }
pending.forEach { if (it.status != Download.QUEUE) it.status = Download.QUEUE } pending.forEach { if (it.status != Download.QUEUE) it.status = Download.QUEUE }
@ -119,8 +121,8 @@ class Downloader(
fun stop(reason: String? = null) { fun stop(reason: String? = null) {
destroySubscriptions() destroySubscriptions()
queue queue
.filter { it.status == Download.DOWNLOADING } .filter { it.status == Download.DOWNLOADING }
.forEach { it.status = Download.ERROR } .forEach { it.status = Download.ERROR }
if (reason != null) { if (reason != null) {
notifier.onWarning(reason) notifier.onWarning(reason)
@ -140,8 +142,8 @@ class Downloader(
fun pause() { fun pause() {
destroySubscriptions() destroySubscriptions()
queue queue
.filter { it.status == Download.DOWNLOADING } .filter { it.status == Download.DOWNLOADING }
.forEach { it.status = Download.QUEUE } .forEach { it.status = Download.QUEUE }
notifier.paused = true notifier.paused = true
} }
@ -156,8 +158,8 @@ class Downloader(
// Needed to update the chapter view // Needed to update the chapter view
if (isNotification) { if (isNotification) {
queue queue
.filter { it.status == Download.QUEUE } .filter { it.status == Download.QUEUE }
.forEach { it.status = Download.NOT_DOWNLOADED } .forEach { it.status = Download.NOT_DOWNLOADED }
} }
queue.clear() queue.clear()
notifier.dismiss() notifier.dismiss()
@ -174,16 +176,19 @@ class Downloader(
subscriptions.clear() subscriptions.clear()
subscriptions += downloadsRelay.concatMapIterable { it } subscriptions += downloadsRelay.concatMapIterable { it }
.concatMap { downloadChapter(it).subscribeOn(Schedulers.io()) } .concatMap { downloadChapter(it).subscribeOn(Schedulers.io()) }
.onBackpressureBuffer() .onBackpressureBuffer()
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe({ .subscribe(
{
completeDownload(it) completeDownload(it)
}, { error -> },
{ error ->
DownloadService.stop(context) DownloadService.stop(context)
Timber.e(error) Timber.e(error)
notifier.onError(error.message) notifier.onError(error.message)
}) }
)
} }
/** /**
@ -212,20 +217,20 @@ class Downloader(
val mangaDir = provider.findMangaDir(manga, source) val mangaDir = provider.findMangaDir(manga, source)
chapters chapters
// Avoid downloading chapters with the same name. // Avoid downloading chapters with the same name.
.distinctBy { it.name } .distinctBy { it.name }
// Filter out those already downloaded. // Filter out those already downloaded.
.filter { mangaDir?.findFile(provider.getChapterDirName(it)) == null } .filter { mangaDir?.findFile(provider.getChapterDirName(it)) == null }
// Add chapters to queue from the start. // Add chapters to queue from the start.
.sortedByDescending { it.source_order } .sortedByDescending { it.source_order }
} }
// Runs in main thread (synchronization needed). // Runs in main thread (synchronization needed).
val chaptersToQueue = chaptersWithoutDir.await() val chaptersToQueue = chaptersWithoutDir.await()
// Filter out those already enqueued. // Filter out those already enqueued.
.filter { chapter -> queue.none { it.chapter.id == chapter.id } } .filter { chapter -> queue.none { it.chapter.id == chapter.id } }
// Create a download for each one. // Create a download for each one.
.map { Download(source, manga, it) } .map { Download(source, manga, it) }
if (chaptersToQueue.isNotEmpty()) { if (chaptersToQueue.isNotEmpty()) {
queue.addAll(chaptersToQueue) queue.addAll(chaptersToQueue)
@ -255,43 +260,43 @@ class Downloader(
val pageListObservable = if (download.pages == null) { val pageListObservable = if (download.pages == null) {
// Pull page list from network and add them to download object // Pull page list from network and add them to download object
download.source.fetchPageList(download.chapter) download.source.fetchPageList(download.chapter)
.doOnNext { pages -> .doOnNext { pages ->
if (pages.isEmpty()) { if (pages.isEmpty()) {
throw Exception("Page list is empty") throw Exception("Page list is empty")
}
download.pages = pages
} }
download.pages = pages
}
} else { } else {
// Or if the page list already exists, start from the file // Or if the page list already exists, start from the file
Observable.just(download.pages!!) Observable.just(download.pages!!)
} }
pageListObservable pageListObservable
.doOnNext { _ -> .doOnNext { _ ->
// Delete all temporary (unfinished) files // Delete all temporary (unfinished) files
tmpDir.listFiles() tmpDir.listFiles()
?.filter { it.name!!.endsWith(".tmp") } ?.filter { it.name!!.endsWith(".tmp") }
?.forEach { it.delete() } ?.forEach { it.delete() }
download.downloadedImages = 0 download.downloadedImages = 0
download.status = Download.DOWNLOADING download.status = Download.DOWNLOADING
} }
// Get all the URLs to the source images, fetch pages if necessary // Get all the URLs to the source images, fetch pages if necessary
.flatMap { download.source.fetchAllImageUrlsFromPageList(it) } .flatMap { download.source.fetchAllImageUrlsFromPageList(it) }
// Start downloading images, consider we can have downloaded images already // Start downloading images, consider we can have downloaded images already
.concatMap { page -> getOrDownloadImage(page, download, tmpDir) } .concatMap { page -> getOrDownloadImage(page, download, tmpDir) }
// Do when page is downloaded. // Do when page is downloaded.
.doOnNext { notifier.onProgressChange(download) } .doOnNext { notifier.onProgressChange(download) }
.toList() .toList()
.map { download } .map { download }
// Do after download completes // Do after download completes
.doOnNext { ensureSuccessfulDownload(download, mangaDir, tmpDir, chapterDirname) } .doOnNext { ensureSuccessfulDownload(download, mangaDir, tmpDir, chapterDirname) }
// If the page list threw, it will resume here // If the page list threw, it will resume here
.onErrorReturn { error -> .onErrorReturn { error ->
download.status = Download.ERROR download.status = Download.ERROR
notifier.onError(error.message, download.chapter.name) notifier.onError(error.message, download.chapter.name)
download download
} }
} }
/** /**
@ -304,8 +309,9 @@ class Downloader(
*/ */
private fun getOrDownloadImage(page: Page, download: Download, tmpDir: UniFile): Observable<Page> { private fun getOrDownloadImage(page: Page, download: Download, tmpDir: UniFile): Observable<Page> {
// If the image URL is empty, do nothing // If the image URL is empty, do nothing
if (page.imageUrl == null) if (page.imageUrl == null) {
return Observable.just(page) return Observable.just(page)
}
val filename = String.format("%03d", page.number) val filename = String.format("%03d", page.number)
val tmpFile = tmpDir.findFile("$filename.tmp") val tmpFile = tmpDir.findFile("$filename.tmp")
@ -317,26 +323,27 @@ class Downloader(
val imageFile = tmpDir.listFiles()!!.find { it.name!!.startsWith("$filename.") } val imageFile = tmpDir.listFiles()!!.find { it.name!!.startsWith("$filename.") }
// If the image is already downloaded, do nothing. Otherwise download from network // 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) Observable.just(imageFile)
else } else {
downloadImage(page, download.source, tmpDir, filename) downloadImage(page, download.source, tmpDir, filename)
}
return pageObservable return pageObservable
// When the image is ready, set image path, progress (just in case) and status // When the image is ready, set image path, progress (just in case) and status
.doOnNext { file -> .doOnNext { file ->
page.uri = file.uri page.uri = file.uri
page.progress = 100 page.progress = 100
download.downloadedImages++ download.downloadedImages++
page.status = Page.READY page.status = Page.READY
} }
.map { page } .map { page }
// Mark this page as error and allow to download the remaining // Mark this page as error and allow to download the remaining
.onErrorReturn { .onErrorReturn {
page.progress = 0 page.progress = 0
page.status = Page.ERROR page.status = Page.ERROR
page page
} }
} }
/** /**
@ -351,21 +358,21 @@ class Downloader(
page.status = Page.DOWNLOAD_IMAGE page.status = Page.DOWNLOAD_IMAGE
page.progress = 0 page.progress = 0
return source.fetchImage(page) return source.fetchImage(page)
.map { response -> .map { response ->
val file = tmpDir.createFile("$filename.tmp") val file = tmpDir.createFile("$filename.tmp")
try { try {
response.body!!.source().saveTo(file.openOutputStream()) response.body!!.source().saveTo(file.openOutputStream())
val extension = getImageExtension(response, file) val extension = getImageExtension(response, file)
file.renameTo("$filename.$extension") file.renameTo("$filename.$extension")
} catch (e: Exception) { } catch (e: Exception) {
response.close() response.close()
file.delete() file.delete()
throw e throw e
}
file
} }
// Retry 3 times, waiting 2, 4 and 8 seconds between attempts. file
.retryWhen(RetryWithDelay(3, { (2 shl it - 1) * 1000 }, Schedulers.trampoline())) }
// 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 { private fun getImageExtension(response: Response, file: UniFile): String {
// Read content type if available. // Read content type if available.
val mime = response.body?.contentType()?.let { ct -> "${ct.type}/${ct.subtype}" } val mime = response.body?.contentType()?.let { ct -> "${ct.type}/${ct.subtype}" }
// Else guess from the uri. // Else guess from the uri.
?: context.contentResolver.getType(file.uri) ?: context.contentResolver.getType(file.uri)
// Else read magic numbers. // Else read magic numbers.
?: ImageUtil.findImageType { file.openInputStream() }?.mime ?: ImageUtil.findImageType { file.openInputStream() }?.mime
return MimeTypeMap.getSingleton().getExtensionFromMimeType(mime) ?: "jpg" return MimeTypeMap.getSingleton().getExtensionFromMimeType(mime) ?: "jpg"
} }
@ -400,7 +407,6 @@ class Downloader(
tmpDir: UniFile, tmpDir: UniFile,
dirname: String dirname: String
) { ) {
// Ensure that the chapter folder has all the images. // Ensure that the chapter folder has all the images.
val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") } val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") }

View File

@ -70,13 +70,13 @@ class DownloadQueue(
} }
fun getActiveDownloads(): Observable<Download> = 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 getStatusObservable(): Observable<Download> = statusSubject.onBackpressureBuffer()
fun getUpdatedObservable(): Observable<List<Download>> = updatedRelay.onBackpressureBuffer() fun getUpdatedObservable(): Observable<List<Download>> = updatedRelay.onBackpressureBuffer()
.startWith(Unit) .startWith(Unit)
.map { this } .map { this }
private fun setPagesFor(download: Download) { private fun setPagesFor(download: Download) {
if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) { if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) {
@ -86,21 +86,21 @@ class DownloadQueue(
fun getProgressObservable(): Observable<Download> { fun getProgressObservable(): Observable<Download> {
return statusSubject.onBackpressureBuffer() return statusSubject.onBackpressureBuffer()
.startWith(getActiveDownloads()) .startWith(getActiveDownloads())
.flatMap { download -> .flatMap { download ->
if (download.status == Download.DOWNLOADING) { if (download.status == Download.DOWNLOADING) {
val pageStatusSubject = PublishSubject.create<Int>() val pageStatusSubject = PublishSubject.create<Int>()
setPagesSubject(download.pages, pageStatusSubject) setPagesSubject(download.pages, pageStatusSubject)
return@flatMap pageStatusSubject return@flatMap pageStatusSubject
.onBackpressureBuffer() .onBackpressureBuffer()
.filter { it == Page.READY } .filter { it == Page.READY }
.map { download } .map { download }
} else if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) { } else if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) {
setPagesSubject(download.pages, null) setPagesSubject(download.pages, null)
}
Observable.just(download)
} }
.filter { it.status == Download.DOWNLOADING } Observable.just(download)
}
.filter { it.status == Download.DOWNLOADING }
} }
private fun setPagesSubject(pages: List<Page>?, subject: PublishSubject<Int>?) { private fun setPagesSubject(pages: List<Page>?, subject: PublishSubject<Int>?) {

View File

@ -25,36 +25,39 @@ class LibraryMangaUrlFetcher(
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) { override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
if (!file.exists()) { if (!file.exists()) {
networkFetcher.loadData(priority, object : DataFetcher.DataCallback<InputStream> { networkFetcher.loadData(
override fun onDataReady(data: InputStream?) { priority,
if (data != null) { object : DataFetcher.DataCallback<InputStream> {
val tmpFile = File(file.path + ".tmp") override fun onDataReady(data: InputStream?) {
try { if (data != null) {
// Retrieve destination stream, create parent folders if needed. val tmpFile = File(file.path + ".tmp")
val output = try { try {
tmpFile.outputStream() // Retrieve destination stream, create parent folders if needed.
} catch (e: FileNotFoundException) { val output = try {
tmpFile.parentFile.mkdirs() tmpFile.outputStream()
tmpFile.outputStream() } catch (e: FileNotFoundException) {
} tmpFile.parentFile.mkdirs()
tmpFile.outputStream()
}
// Copy the file and rename to the original. // Copy the file and rename to the original.
data.use { output.use { data.copyTo(output) } } data.use { output.use { data.copyTo(output) } }
tmpFile.renameTo(file) tmpFile.renameTo(file)
loadFromFile(callback) loadFromFile(callback)
} catch (e: Exception) { } catch (e: Exception) {
tmpFile.delete() tmpFile.delete()
callback.onLoadFailed(e) 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 { } else {
loadFromFile(callback) loadFromFile(callback)
} }

View File

@ -27,8 +27,10 @@ class TachiGlideModule : AppGlideModule() {
override fun applyOptions(context: Context, builder: GlideBuilder) { override fun applyOptions(context: Context, builder: GlideBuilder) {
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 50 * 1024 * 1024)) builder.setDiskCache(InternalCacheDiskCacheFactory(context, 50 * 1024 * 1024))
builder.setDefaultRequestOptions(RequestOptions().format(DecodeFormat.PREFER_RGB_565)) builder.setDefaultRequestOptions(RequestOptions().format(DecodeFormat.PREFER_RGB_565))
builder.setDefaultTransitionOptions(Drawable::class.java, builder.setDefaultTransitionOptions(
DrawableTransitionOptions.withCrossFade()) Drawable::class.java,
DrawableTransitionOptions.withCrossFade()
)
} }
override fun registerComponents(context: Context, glide: Glide, registry: Registry) { 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.replace(GlideUrl::class.java, InputStream::class.java, networkFactory)
registry.append(MangaThumbnail::class.java, InputStream::class.java, MangaThumbnailModelLoader.Factory()) registry.append(MangaThumbnail::class.java, InputStream::class.java, MangaThumbnailModelLoader.Factory())
registry.append(InputStream::class.java, InputStream::class.java, PassthroughModelLoader registry.append(
.Factory()) InputStream::class.java, InputStream::class.java,
PassthroughModelLoader
.Factory()
)
} }
} }

View File

@ -14,7 +14,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) : class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) :
Worker(context, workerParams) { Worker(context, workerParams) {
override fun doWork(): Result { override fun doWork(): Result {
LibraryUpdateService.start(context) LibraryUpdateService.start(context)
@ -30,22 +30,24 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
if (interval > 0) { if (interval > 0) {
val restrictions = preferences.libraryUpdateRestriction()!! val restrictions = preferences.libraryUpdateRestriction()!!
val acRestriction = "ac" in restrictions val acRestriction = "ac" in restrictions
val wifiRestriction = if ("wifi" in restrictions) val wifiRestriction = if ("wifi" in restrictions) {
NetworkType.UNMETERED NetworkType.UNMETERED
else } else {
NetworkType.CONNECTED NetworkType.CONNECTED
}
val constraints = Constraints.Builder() val constraints = Constraints.Builder()
.setRequiredNetworkType(wifiRestriction) .setRequiredNetworkType(wifiRestriction)
.setRequiresCharging(acRestriction) .setRequiresCharging(acRestriction)
.build() .build()
val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>( val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>(
interval.toLong(), TimeUnit.HOURS, interval.toLong(), TimeUnit.HOURS,
10, TimeUnit.MINUTES) 10, TimeUnit.MINUTES
.addTag(TAG) )
.setConstraints(constraints) .addTag(TAG)
.build() .setConstraints(constraints)
.build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request) WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request)
} else { } else {

View File

@ -8,8 +8,9 @@ import eu.kanade.tachiyomi.data.database.models.Manga
object LibraryUpdateRanker { object LibraryUpdateRanker {
val rankingScheme = listOf( val rankingScheme = listOf(
(this::lexicographicRanking)(), (this::lexicographicRanking)(),
(this::latestFirstRanking)()) (this::latestFirstRanking)()
)
/** /**
* Provides a total ordering over all the Mangas. * Provides a total ordering over all the Mangas.
@ -22,7 +23,7 @@ object LibraryUpdateRanker {
*/ */
fun latestFirstRanking(): Comparator<Manga> { fun latestFirstRanking(): Comparator<Manga> {
return Comparator { mangaFirst: Manga, return Comparator { mangaFirst: Manga,
mangaSecond: Manga -> mangaSecond: Manga ->
compareValues(mangaSecond.last_update, mangaFirst.last_update) compareValues(mangaSecond.last_update, mangaFirst.last_update)
} }
} }
@ -35,7 +36,7 @@ object LibraryUpdateRanker {
*/ */
fun lexicographicRanking(): Comparator<Manga> { fun lexicographicRanking(): Comparator<Manga> {
return Comparator { mangaFirst: Manga, return Comparator { mangaFirst: Manga,
mangaSecond: Manga -> mangaSecond: Manga ->
compareValues(mangaFirst.title, mangaSecond.title) compareValues(mangaFirst.title, mangaSecond.title)
} }
} }

View File

@ -184,7 +184,8 @@ class LibraryUpdateService(
super.onCreate() super.onCreate()
startForeground(Notifications.ID_LIBRARY_PROGRESS, progressNotificationBuilder.build()) startForeground(Notifications.ID_LIBRARY_PROGRESS, progressNotificationBuilder.build())
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock( wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock") PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock"
)
wakeLock.acquire() wakeLock.acquire()
} }
@ -218,33 +219,37 @@ class LibraryUpdateService(
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent == null) return START_NOT_STICKY if (intent == null) return START_NOT_STICKY
val target = intent.getSerializableExtra(KEY_TARGET) as? Target val target = intent.getSerializableExtra(KEY_TARGET) as? Target
?: return START_NOT_STICKY ?: return START_NOT_STICKY
// Unsubscribe from any previous subscription if needed. // Unsubscribe from any previous subscription if needed.
subscription?.unsubscribe() subscription?.unsubscribe()
// Update favorite manga. Destroy service when completed or in case of an error. // Update favorite manga. Destroy service when completed or in case of an error.
subscription = Observable subscription = Observable
.defer { .defer {
val selectedScheme = preferences.libraryUpdatePrioritization().get() val selectedScheme = preferences.libraryUpdatePrioritization().get()
val mangaList = getMangaToUpdate(intent, target) val mangaList = getMangaToUpdate(intent, target)
.sortedWith(rankingScheme[selectedScheme]) .sortedWith(rankingScheme[selectedScheme])
// Update either chapter list or manga details. // Update either chapter list or manga details.
when (target) { when (target) {
Target.CHAPTERS -> updateChapterList(mangaList) Target.CHAPTERS -> updateChapterList(mangaList)
Target.DETAILS -> updateDetails(mangaList) Target.DETAILS -> updateDetails(mangaList)
Target.TRACKING -> updateTrackings(mangaList) Target.TRACKING -> updateTrackings(mangaList)
}
} }
.subscribeOn(Schedulers.io()) }
.subscribe({ .subscribeOn(Schedulers.io())
}, { .subscribe(
{
},
{
Timber.e(it) Timber.e(it)
stopSelf(startId) stopSelf(startId)
}, { },
{
stopSelf(startId) stopSelf(startId)
}) }
)
return START_REDELIVER_INTENT return START_REDELIVER_INTENT
} }
@ -259,16 +264,17 @@ class LibraryUpdateService(
fun getMangaToUpdate(intent: Intent, target: Target): List<LibraryManga> { fun getMangaToUpdate(intent: Intent, target: Target): List<LibraryManga> {
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1) val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
var listToUpdate = if (categoryId != -1) var listToUpdate = if (categoryId != -1) {
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId } db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
else { } else {
val categoriesToUpdate = preferences.libraryUpdateCategories().get().map(String::toInt) val categoriesToUpdate = preferences.libraryUpdateCategories().get().map(String::toInt)
if (categoriesToUpdate.isNotEmpty()) if (categoriesToUpdate.isNotEmpty()) {
db.getLibraryMangas().executeAsBlocking() db.getLibraryMangas().executeAsBlocking()
.filter { it.category in categoriesToUpdate } .filter { it.category in categoriesToUpdate }
.distinctBy { it.id } .distinctBy { it.id }
else } else {
db.getLibraryMangas().executeAsBlocking().distinctBy { it.id } db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
}
} }
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) { if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED } listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
@ -302,55 +308,57 @@ class LibraryUpdateService(
// Emit each manga and update it sequentially. // Emit each manga and update it sequentially.
return Observable.from(mangaToUpdate) return Observable.from(mangaToUpdate)
// Notify manga that will update. // Notify manga that will update.
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) } .doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) }
// Update the chapters of the manga. // Update the chapters of the manga.
.concatMap { manga -> .concatMap { manga ->
updateManga(manga) updateManga(manga)
// If there's any error, return empty update and continue. // If there's any error, return empty update and continue.
.onErrorReturn { .onErrorReturn {
failedUpdates.add(manga) failedUpdates.add(manga)
Pair(emptyList(), emptyList()) Pair(emptyList(), emptyList())
} }
// Filter out mangas without new chapters (or failed). // Filter out mangas without new chapters (or failed).
.filter { pair -> pair.first.isNotEmpty() } .filter { pair -> pair.first.isNotEmpty() }
.doOnNext { .doOnNext {
if (downloadNew && (categoriesToDownload.isEmpty() || if (downloadNew && (
manga.category in categoriesToDownload)) { categoriesToDownload.isEmpty() ||
manga.category in categoriesToDownload
downloadChapters(manga, it.first) )
hasDownloads = true ) {
} 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)
} }
} }
// Convert to the manga that contains new chapters.
if (failedUpdates.isNotEmpty()) { .map {
Timber.e("Failed updating: ${failedUpdates.map { it.title }}") 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>) { fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
@ -373,7 +381,7 @@ class LibraryUpdateService(
fun updateManga(manga: Manga): Observable<Pair<List<Chapter>, List<Chapter>>> { fun updateManga(manga: Manga): Observable<Pair<List<Chapter>, List<Chapter>>> {
val source = sourceManager.get(manga.source) as? HttpSource ?: return Observable.empty() val source = sourceManager.get(manga.source) as? HttpSource ?: return Observable.empty()
return source.fetchChapterList(manga) 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. // Emit each manga and update it sequentially.
return Observable.from(mangaToUpdate) return Observable.from(mangaToUpdate)
// Notify manga that will update. // Notify manga that will update.
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) } .doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) }
// Update the details of the manga. // Update the details of the manga.
.concatMap { manga -> .concatMap { manga ->
val source = sourceManager.get(manga.source) as? HttpSource val source = sourceManager.get(manga.source) as? HttpSource
?: return@concatMap Observable.empty<LibraryManga>() ?: return@concatMap Observable.empty<LibraryManga>()
source.fetchMangaDetails(manga) source.fetchMangaDetails(manga)
.map { networkManga -> .map { networkManga ->
manga.copyFrom(networkManga) manga.copyFrom(networkManga)
db.insertManga(manga).executeAsBlocking() db.insertManga(manga).executeAsBlocking()
manga manga
} }
.onErrorReturn { manga } .onErrorReturn { manga }
} }
.doOnCompleted { .doOnCompleted {
cancelProgressNotification() cancelProgressNotification()
} }
} }
/** /**
@ -421,28 +429,28 @@ class LibraryUpdateService(
// Emit each manga and update it sequentially. // Emit each manga and update it sequentially.
return Observable.from(mangaToUpdate) return Observable.from(mangaToUpdate)
// Notify manga that will update. // Notify manga that will update.
.doOnNext { showProgressNotification(it, count++, mangaToUpdate.size) } .doOnNext { showProgressNotification(it, count++, mangaToUpdate.size) }
// Update the tracking details. // Update the tracking details.
.concatMap { manga -> .concatMap { manga ->
val tracks = db.getTracks(manga).executeAsBlocking() val tracks = db.getTracks(manga).executeAsBlocking()
Observable.from(tracks) Observable.from(tracks)
.concatMap { track -> .concatMap { track ->
val service = trackManager.getService(track.sync_id) val service = trackManager.getService(track.sync_id)
if (service != null && service in loggedServices) { if (service != null && service in loggedServices) {
service.refresh(track) service.refresh(track)
.doOnNext { db.insertTrack(it).executeAsBlocking() } .doOnNext { db.insertTrack(it).executeAsBlocking() }
.onErrorReturn { track } .onErrorReturn { track }
} else { } else {
Observable.empty() Observable.empty()
} }
} }
.map { manga } .map { manga }
} }
.doOnCompleted { .doOnCompleted {
cancelProgressNotification() cancelProgressNotification()
} }
} }
/** /**
@ -453,15 +461,19 @@ class LibraryUpdateService(
* @param total the total progress. * @param total the total progress.
*/ */
private fun showProgressNotification(manga: Manga, current: Int, total: Int) { 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) getString(R.string.notification_check_updates)
else } else {
manga.title manga.title
}
notificationManager.notify(Notifications.ID_LIBRARY_PROGRESS, progressNotificationBuilder notificationManager.notify(
Notifications.ID_LIBRARY_PROGRESS,
progressNotificationBuilder
.setContentTitle(title) .setContentTitle(title)
.setProgress(total, current, false) .setProgress(total, current, false)
.build()) .build()
)
} }
/** /**
@ -476,31 +488,38 @@ class LibraryUpdateService(
NotificationManagerCompat.from(this).apply { NotificationManagerCompat.from(this).apply {
// Parent group notification // Parent group notification
notify(Notifications.ID_NEW_CHAPTERS, notification(Notifications.CHANNEL_NEW_CHAPTERS) { notify(
setContentTitle(getString(R.string.notification_new_chapters)) Notifications.ID_NEW_CHAPTERS,
if (updates.size == 1 && !preferences.hideNotificationContent()) { notification(Notifications.CHANNEL_NEW_CHAPTERS) {
setContentText(updates.first().first.title.chop(NOTIF_TITLE_MAX_LEN)) setContentTitle(getString(R.string.notification_new_chapters))
} else { if (updates.size == 1 && !preferences.hideNotificationContent()) {
setContentText(resources.getQuantityString(R.plurals.notification_new_chapters_summary, updates.size, updates.size)) 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()) { if (!preferences.hideNotificationContent()) {
setStyle(NotificationCompat.BigTextStyle().bigText(updates.joinToString("\n") { setStyle(
it.first.title.chop(NOTIF_TITLE_MAX_LEN) 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 // Per-manga notification
if (!preferences.hideNotificationContent()) { if (!preferences.hideNotificationContent()) {
@ -536,13 +555,21 @@ class LibraryUpdateService(
setAutoCancel(true) setAutoCancel(true)
// Mark chapters as read action // Mark chapters as read action
addAction(R.drawable.ic_glasses_black_24dp, getString(R.string.action_mark_as_read), addAction(
NotificationReceiver.markAsReadPendingBroadcast(this@LibraryUpdateService, R.drawable.ic_glasses_black_24dp, getString(R.string.action_mark_as_read),
manga, chapters, Notifications.ID_NEW_CHAPTERS)) NotificationReceiver.markAsReadPendingBroadcast(
this@LibraryUpdateService,
manga, chapters, Notifications.ID_NEW_CHAPTERS
)
)
// View chapters action // View chapters action
addAction(R.drawable.ic_book_24dp, getString(R.string.action_view_chapters), addAction(
NotificationReceiver.openChapterPendingActivity(this@LibraryUpdateService, R.drawable.ic_book_24dp, getString(R.string.action_view_chapters),
manga, Notifications.ID_NEW_CHAPTERS)) NotificationReceiver.openChapterPendingActivity(
this@LibraryUpdateService,
manga, Notifications.ID_NEW_CHAPTERS
)
)
} }
} }
@ -556,28 +583,31 @@ class LibraryUpdateService(
private fun getMangaIcon(manga: Manga): Bitmap? { private fun getMangaIcon(manga: Manga): Bitmap? {
return try { return try {
Glide.with(this) Glide.with(this)
.asBitmap() .asBitmap()
.load(manga.toMangaThumbnail()) .load(manga.toMangaThumbnail())
.dontTransform() .dontTransform()
.centerCrop() .centerCrop()
.circleCrop() .circleCrop()
.override(NOTIF_ICON_SIZE, NOTIF_ICON_SIZE) .override(NOTIF_ICON_SIZE, NOTIF_ICON_SIZE)
.submit() .submit()
.get() .get()
} catch (e: Exception) { } catch (e: Exception) {
null null
} }
} }
private fun getNewChaptersDescription(chapters: Array<Chapter>): String { private fun getNewChaptersDescription(chapters: Array<Chapter>): String {
val formatter = DecimalFormat("#.###", DecimalFormatSymbols() val formatter = DecimalFormat(
.apply { decimalSeparator = '.' }) "#.###",
DecimalFormatSymbols()
.apply { decimalSeparator = '.' }
)
val displayableChapterNumbers = chapters val displayableChapterNumbers = chapters
.filter { it.isRecognizedNumber } .filter { it.isRecognizedNumber }
.sortedBy { it.chapter_number } .sortedBy { it.chapter_number }
.map { formatter.format(it.chapter_number) } .map { formatter.format(it.chapter_number) }
.toSet() .toSet()
return when (displayableChapterNumbers.size) { return when (displayableChapterNumbers.size) {
// No sensible chapter numbers to show (i.e. no chapters have parsed chapter number) // No sensible chapter numbers to show (i.e. no chapters have parsed chapter number)

View File

@ -54,22 +54,35 @@ class NotificationReceiver : BroadcastReceiver() {
// Clear the download queue // Clear the download queue
ACTION_CLEAR_DOWNLOADS -> downloadManager.clearQueue(true) ACTION_CLEAR_DOWNLOADS -> downloadManager.clearQueue(true)
// Launch share activity and dismiss notification // Launch share activity and dismiss notification
ACTION_SHARE_IMAGE -> shareImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION), ACTION_SHARE_IMAGE ->
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)) shareImage(
context, intent.getStringExtra(EXTRA_FILE_LOCATION),
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
)
// Delete image from path and dismiss notification // Delete image from path and dismiss notification
ACTION_DELETE_IMAGE -> deleteImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION), ACTION_DELETE_IMAGE ->
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)) deleteImage(
context, intent.getStringExtra(EXTRA_FILE_LOCATION),
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
)
// Share backup file // Share backup file
ACTION_SHARE_BACKUP -> shareBackup(context, intent.getParcelableExtra(EXTRA_URI), ACTION_SHARE_BACKUP ->
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)) shareBackup(
ACTION_CANCEL_RESTORE -> cancelRestore(context, context, intent.getParcelableExtra(EXTRA_URI),
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)) intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
)
ACTION_CANCEL_RESTORE -> cancelRestore(
context,
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
)
// Cancel library update and dismiss notification // Cancel library update and dismiss notification
ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context, Notifications.ID_LIBRARY_PROGRESS) ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context, Notifications.ID_LIBRARY_PROGRESS)
// Open reader activity // Open reader activity
ACTION_OPEN_CHAPTER -> { ACTION_OPEN_CHAPTER -> {
openChapter(context, intent.getLongExtra(EXTRA_MANGA_ID, -1), openChapter(
intent.getLongExtra(EXTRA_CHAPTER_ID, -1)) context, intent.getLongExtra(EXTRA_MANGA_ID, -1),
intent.getLongExtra(EXTRA_CHAPTER_ID, -1)
)
} }
// Mark updated manga chapters as read // Mark updated manga chapters as read
ACTION_MARK_AS_READ -> { ACTION_MARK_AS_READ -> {
@ -208,19 +221,19 @@ class NotificationReceiver : BroadcastReceiver() {
launchIO { launchIO {
chapterUrls.mapNotNull { db.getChapter(it, mangaId).executeAsBlocking() } chapterUrls.mapNotNull { db.getChapter(it, mangaId).executeAsBlocking() }
.forEach { .forEach {
it.read = true it.read = true
db.updateChapterProgress(it).executeAsBlocking() db.updateChapterProgress(it).executeAsBlocking()
if (preferences.removeAfterMarkedAsRead()) { if (preferences.removeAfterMarkedAsRead()) {
val manga = db.getManga(mangaId).executeAsBlocking() val manga = db.getManga(mangaId).executeAsBlocking()
if (manga != null) { if (manga != null) {
val source = sourceManager.get(manga.source) val source = sourceManager.get(manga.source)
if (source != null) { if (source != null) {
downloadManager.deleteChapters(listOf(it), manga, source) downloadManager.deleteChapters(listOf(it), manga, source)
}
} }
} }
} }
}
} }
} }
@ -427,11 +440,11 @@ class NotificationReceiver : BroadcastReceiver() {
*/ */
internal fun openChapterPendingActivity(context: Context, manga: Manga, groupId: Int): PendingIntent { internal fun openChapterPendingActivity(context: Context, manga: Manga, groupId: Int): PendingIntent {
val newIntent = val newIntent =
Intent(context, MainActivity::class.java).setAction(MainActivity.SHORTCUT_MANGA) Intent(context, MainActivity::class.java).setAction(MainActivity.SHORTCUT_MANGA)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
.putExtra(MangaController.MANGA_EXTRA, manga.id) .putExtra(MangaController.MANGA_EXTRA, manga.id)
.putExtra("notificationId", manga.id.hashCode()) .putExtra("notificationId", manga.id.hashCode())
.putExtra("groupId", groupId) .putExtra("groupId", groupId)
return PendingIntent.getActivity(context, manga.id.hashCode(), newIntent, PendingIntent.FLAG_UPDATE_CURRENT) return PendingIntent.getActivity(context, manga.id.hashCode(), newIntent, PendingIntent.FLAG_UPDATE_CURRENT)
} }

View File

@ -61,24 +61,36 @@ object Notifications {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val channels = listOf( val channels = listOf(
NotificationChannel(CHANNEL_COMMON, context.getString(R.string.channel_common), NotificationChannel(
NotificationManager.IMPORTANCE_LOW), CHANNEL_COMMON, context.getString(R.string.channel_common),
NotificationChannel(CHANNEL_LIBRARY, context.getString(R.string.channel_library), NotificationManager.IMPORTANCE_LOW
NotificationManager.IMPORTANCE_LOW).apply { ),
setShowBadge(false) NotificationChannel(
}, CHANNEL_LIBRARY, context.getString(R.string.channel_library),
NotificationChannel(CHANNEL_DOWNLOADER, context.getString(R.string.channel_downloader), NotificationManager.IMPORTANCE_LOW
NotificationManager.IMPORTANCE_LOW).apply { ).apply {
setShowBadge(false) setShowBadge(false)
}, },
NotificationChannel(CHANNEL_NEW_CHAPTERS, context.getString(R.string.channel_new_chapters), NotificationChannel(
NotificationManager.IMPORTANCE_DEFAULT), CHANNEL_DOWNLOADER, context.getString(R.string.channel_downloader),
NotificationChannel(CHANNEL_UPDATES_TO_EXTS, context.getString(R.string.channel_ext_updates), NotificationManager.IMPORTANCE_LOW
NotificationManager.IMPORTANCE_DEFAULT), ).apply {
NotificationChannel(CHANNEL_BACKUP_RESTORE, context.getString(R.string.channel_backup_restore), setShowBadge(false)
NotificationManager.IMPORTANCE_HIGH).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) context.notificationManager.createNotificationChannels(channels)
} }

View File

@ -45,12 +45,20 @@ class PreferencesHelper(val context: Context) {
private val flowPrefs = FlowSharedPreferences(prefs) private val flowPrefs = FlowSharedPreferences(prefs)
private val defaultDownloadsDir = Uri.fromFile( private val defaultDownloadsDir = Uri.fromFile(
File(Environment.getExternalStorageDirectory().absolutePath + File.separator + File(
context.getString(R.string.app_name), "downloads")) Environment.getExternalStorageDirectory().absolutePath + File.separator +
context.getString(R.string.app_name),
"downloads"
)
)
private val defaultBackupDir = Uri.fromFile( private val defaultBackupDir = Uri.fromFile(
File(Environment.getExternalStorageDirectory().absolutePath + File.separator + File(
context.getString(R.string.app_name), "backup")) Environment.getExternalStorageDirectory().absolutePath + File.separator +
context.getString(R.string.app_name),
"backup"
)
)
fun startScreen() = prefs.getInt(Keys.startScreen, 1) fun startScreen() = prefs.getInt(Keys.startScreen, 1)
@ -148,9 +156,9 @@ class PreferencesHelper(val context: Context) {
fun setTrackCredentials(sync: TrackService, username: String, password: String) { fun setTrackCredentials(sync: TrackService, username: String, password: String) {
prefs.edit() prefs.edit()
.putString(Keys.trackUsername(sync.id), username) .putString(Keys.trackUsername(sync.id), username)
.putString(Keys.trackPassword(sync.id), password) .putString(Keys.trackPassword(sync.id), password)
.apply() .apply()
} }
fun trackToken(sync: TrackService) = flowPrefs.getString(Keys.trackToken(sync.id), "") fun trackToken(sync: TrackService) = flowPrefs.getString(Keys.trackToken(sync.id), "")

View File

@ -63,7 +63,7 @@ abstract class TrackService(val id: Int) {
open val isLogged: Boolean open val isLogged: Boolean
get() = getUsername().isNotEmpty() && get() = getUsername().isNotEmpty() &&
getPassword().isNotEmpty() getPassword().isNotEmpty()
fun getUsername() = preferences.trackUsername(this)!! fun getUsername() = preferences.trackUsername(this)!!

View File

@ -150,18 +150,18 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
override fun bind(track: Track): Observable<Track> { override fun bind(track: Track): Observable<Track> {
return api.findLibManga(track, getUsername().toInt()) return api.findLibManga(track, getUsername().toInt())
.flatMap { remoteTrack -> .flatMap { remoteTrack ->
if (remoteTrack != null) { if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack) track.copyPersonalFrom(remoteTrack)
track.library_id = remoteTrack.library_id track.library_id = remoteTrack.library_id
update(track) update(track)
} else { } else {
// Set default fields if it's not found in the list // Set default fields if it's not found in the list
track.score = DEFAULT_SCORE.toFloat() track.score = DEFAULT_SCORE.toFloat()
track.status = DEFAULT_STATUS track.status = DEFAULT_STATUS
add(track) add(track)
}
} }
}
} }
override fun search(query: String): Observable<List<TrackSearch>> { 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> { override fun refresh(track: Track): Observable<Track> {
return api.getLibManga(track, getUsername().toInt()) return api.getLibManga(track, getUsername().toInt())
.map { remoteTrack -> .map { remoteTrack ->
track.copyPersonalFrom(remoteTrack) track.copyPersonalFrom(remoteTrack)
track.total_chapters = remoteTrack.total_chapters track.total_chapters = remoteTrack.total_chapters
track track
} }
} }
override fun login(username: String, password: String) = login(password) override fun login(username: String, password: String) = login(password)

View File

@ -25,7 +25,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
private val authClient = client.newBuilder().addInterceptor(interceptor).build() private val authClient = client.newBuilder().addInterceptor(interceptor).build()
fun addLibManga(track: Track): Observable<Track> { fun addLibManga(track: Track): Observable<Track> {
val query = """ val query =
"""
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) { |mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) { |SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) {
| id | id
@ -34,35 +35,36 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|} |}
|""".trimMargin() |""".trimMargin()
val variables = jsonObject( val variables = jsonObject(
"mangaId" to track.media_id, "mangaId" to track.media_id,
"progress" to track.last_chapter_read, "progress" to track.last_chapter_read,
"status" to track.toAnilistStatus() "status" to track.toAnilistStatus()
) )
val payload = jsonObject( val payload = jsonObject(
"query" to query, "query" to query,
"variables" to variables "variables" to variables
) )
val body = payload.toString().toRequestBody(jsonMime) val body = payload.toString().toRequestBody(jsonMime)
val request = Request.Builder() val request = Request.Builder()
.url(apiUrl) .url(apiUrl)
.post(body) .post(body)
.build() .build()
return authClient.newCall(request) return authClient.newCall(request)
.asObservableSuccess() .asObservableSuccess()
.map { netResponse -> .map { netResponse ->
val responseBody = netResponse.body?.string().orEmpty() val responseBody = netResponse.body?.string().orEmpty()
netResponse.close() netResponse.close()
if (responseBody.isEmpty()) { if (responseBody.isEmpty()) {
throw Exception("Null Response") throw Exception("Null Response")
}
val response = JsonParser.parseString(responseBody).obj
track.library_id = response["data"]["SaveMediaListEntry"]["id"].asLong
track
} }
val response = JsonParser.parseString(responseBody).obj
track.library_id = response["data"]["SaveMediaListEntry"]["id"].asLong
track
}
} }
fun updateLibManga(track: Track): Observable<Track> { fun updateLibManga(track: Track): Observable<Track> {
val query = """ val query =
"""
|mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) { |mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) {
|SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) { |SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) {
|id |id
@ -72,29 +74,30 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|} |}
|""".trimMargin() |""".trimMargin()
val variables = jsonObject( val variables = jsonObject(
"listId" to track.library_id, "listId" to track.library_id,
"progress" to track.last_chapter_read, "progress" to track.last_chapter_read,
"status" to track.toAnilistStatus(), "status" to track.toAnilistStatus(),
"score" to track.score.toInt() "score" to track.score.toInt()
) )
val payload = jsonObject( val payload = jsonObject(
"query" to query, "query" to query,
"variables" to variables "variables" to variables
) )
val body = payload.toString().toRequestBody(jsonMime) val body = payload.toString().toRequestBody(jsonMime)
val request = Request.Builder() val request = Request.Builder()
.url(apiUrl) .url(apiUrl)
.post(body) .post(body)
.build() .build()
return authClient.newCall(request) return authClient.newCall(request)
.asObservableSuccess() .asObservableSuccess()
.map { .map {
track track
} }
} }
fun search(search: String): Observable<List<TrackSearch>> { fun search(search: String): Observable<List<TrackSearch>> {
val query = """ val query =
"""
|query Search(${'$'}query: String) { |query Search(${'$'}query: String) {
|Page (perPage: 50) { |Page (perPage: 50) {
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) { |media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
@ -119,35 +122,36 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|} |}
|""".trimMargin() |""".trimMargin()
val variables = jsonObject( val variables = jsonObject(
"query" to search "query" to search
) )
val payload = jsonObject( val payload = jsonObject(
"query" to query, "query" to query,
"variables" to variables "variables" to variables
) )
val body = payload.toString().toRequestBody(jsonMime) val body = payload.toString().toRequestBody(jsonMime)
val request = Request.Builder() val request = Request.Builder()
.url(apiUrl) .url(apiUrl)
.post(body) .post(body)
.build() .build()
return authClient.newCall(request) return authClient.newCall(request)
.asObservableSuccess() .asObservableSuccess()
.map { netResponse -> .map { netResponse ->
val responseBody = netResponse.body?.string().orEmpty() val responseBody = netResponse.body?.string().orEmpty()
if (responseBody.isEmpty()) { if (responseBody.isEmpty()) {
throw Exception("Null Response") 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() }
} }
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?> { fun findLibManga(track: Track, userid: Int): Observable<Track?> {
val query = """ val query =
"""
|query (${'$'}id: Int!, ${'$'}manga_id: Int!) { |query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
|Page { |Page {
|mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) { |mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
@ -178,37 +182,37 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|} |}
|""".trimMargin() |""".trimMargin()
val variables = jsonObject( val variables = jsonObject(
"id" to userid, "id" to userid,
"manga_id" to track.media_id "manga_id" to track.media_id
) )
val payload = jsonObject( val payload = jsonObject(
"query" to query, "query" to query,
"variables" to variables "variables" to variables
) )
val body = payload.toString().toRequestBody(jsonMime) val body = payload.toString().toRequestBody(jsonMime)
val request = Request.Builder() val request = Request.Builder()
.url(apiUrl) .url(apiUrl)
.post(body) .post(body)
.build() .build()
return authClient.newCall(request) return authClient.newCall(request)
.asObservableSuccess() .asObservableSuccess()
.map { netResponse -> .map { netResponse ->
val responseBody = netResponse.body?.string().orEmpty() val responseBody = netResponse.body?.string().orEmpty()
if (responseBody.isEmpty()) { if (responseBody.isEmpty()) {
throw Exception("Null Response") 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()
} }
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> { fun getLibManga(track: Track, userid: Int): Observable<Track> {
return findLibManga(track, userid) 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 { fun createOAuth(token: String): OAuth {
@ -216,7 +220,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
} }
fun getCurrentUser(): Observable<Pair<Int, String>> { fun getCurrentUser(): Observable<Pair<Int, String>> {
val query = """ val query =
"""
|query User { |query User {
|Viewer { |Viewer {
|id |id
@ -227,41 +232,48 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|} |}
|""".trimMargin() |""".trimMargin()
val payload = jsonObject( val payload = jsonObject(
"query" to query "query" to query
) )
val body = payload.toString().toRequestBody(jsonMime) val body = payload.toString().toRequestBody(jsonMime)
val request = Request.Builder() val request = Request.Builder()
.url(apiUrl) .url(apiUrl)
.post(body) .post(body)
.build() .build()
return authClient.newCall(request) return authClient.newCall(request)
.asObservableSuccess() .asObservableSuccess()
.map { netResponse -> .map { netResponse ->
val responseBody = netResponse.body?.string().orEmpty() val responseBody = netResponse.body?.string().orEmpty()
if (responseBody.isEmpty()) { if (responseBody.isEmpty()) {
throw Exception("Null Response") 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)
} }
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 { private fun jsonToALManga(struct: JsonObject): ALManga {
val date = try { val date = try {
val date = Calendar.getInstance() val date = Calendar.getInstance()
date.set(struct["startDate"]["year"].nullInt ?: 0, (struct["startDate"]["month"].nullInt date.set(
?: 0) - 1, struct["startDate"]["year"].nullInt ?: 0,
struct["startDate"]["day"].nullInt ?: 0) (
struct["startDate"]["month"].nullInt
?: 0
) - 1,
struct["startDate"]["day"].nullInt ?: 0
)
date.timeInMillis date.timeInMillis
} catch (_: Exception) { } catch (_: Exception) {
0L 0L
} }
return ALManga(struct["id"].asInt, struct["title"]["romaji"].asString, struct["coverImage"]["large"].asString, return ALManga(
struct["description"].nullString.orEmpty(), struct["type"].asString, struct["status"].asString, struct["id"].asInt, struct["title"]["romaji"].asString, struct["coverImage"]["large"].asString,
date, struct["chapters"].nullInt ?: 0) struct["description"].nullString.orEmpty(), struct["type"].asString, struct["status"].asString,
date, struct["chapters"].nullInt ?: 0
)
} }
private fun jsonToALUserManga(struct: JsonObject): ALUserManga { 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() fun authUrl() = Uri.parse("${baseUrl}oauth/authorize").buildUpon()
.appendQueryParameter("client_id", clientId) .appendQueryParameter("client_id", clientId)
.appendQueryParameter("response_type", "token") .appendQueryParameter("response_type", "token")
.build() .build()
} }
} }

View File

@ -38,8 +38,8 @@ class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Int
// Add the authorization header to the original request. // Add the authorization header to the original request.
val authRequest = originalRequest.newBuilder() val authRequest = originalRequest.newBuilder()
.addHeader("Authorization", "Bearer ${oauth!!.access_token}") .addHeader("Authorization", "Bearer ${oauth!!.access_token}")
.build() .build()
return chain.proceed(authRequest) return chain.proceed(authRequest)
} }

View File

@ -39,23 +39,23 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
override fun bind(track: Track): Observable<Track> { override fun bind(track: Track): Observable<Track> {
return api.statusLibManga(track) return api.statusLibManga(track)
.flatMap { .flatMap {
api.findLibManga(track).flatMap { remoteTrack -> api.findLibManga(track).flatMap { remoteTrack ->
if (remoteTrack != null && it != null) { if (remoteTrack != null && it != null) {
track.copyPersonalFrom(remoteTrack) track.copyPersonalFrom(remoteTrack)
track.library_id = remoteTrack.library_id track.library_id = remoteTrack.library_id
track.status = remoteTrack.status track.status = remoteTrack.status
track.last_chapter_read = remoteTrack.last_chapter_read track.last_chapter_read = remoteTrack.last_chapter_read
refresh(track) refresh(track)
} else { } else {
// Set default fields if it's not found in the list // Set default fields if it's not found in the list
track.score = DEFAULT_SCORE.toFloat() track.score = DEFAULT_SCORE.toFloat()
track.status = DEFAULT_STATUS track.status = DEFAULT_STATUS
add(track) add(track)
update(track) update(track)
}
} }
} }
}
} }
override fun search(query: String): Observable<List<TrackSearch>> { 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> { override fun refresh(track: Track): Observable<Track> {
return api.statusLibManga(track) return api.statusLibManga(track)
.flatMap { .flatMap {
track.copyPersonalFrom(it!!) track.copyPersonalFrom(it!!)
api.findLibManga(track) api.findLibManga(track)
.map { remoteTrack -> .map { remoteTrack ->
if (remoteTrack != null) { if (remoteTrack != null) {
track.total_chapters = remoteTrack.total_chapters track.total_chapters = remoteTrack.total_chapters
track.status = remoteTrack.status track.status = remoteTrack.status
} }
track track
} }
} }
} }
override fun getLogo() = R.drawable.ic_tracker_bangumi override fun getLogo() = R.drawable.ic_tracker_bangumi

View File

@ -26,73 +26,74 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
fun addLibManga(track: Track): Observable<Track> { fun addLibManga(track: Track): Observable<Track> {
val body = FormBody.Builder() val body = FormBody.Builder()
.add("rating", track.score.toInt().toString()) .add("rating", track.score.toInt().toString())
.add("status", track.toBangumiStatus()) .add("status", track.toBangumiStatus())
.build() .build()
val request = Request.Builder() val request = Request.Builder()
.url("$apiUrl/collection/${track.media_id}/update") .url("$apiUrl/collection/${track.media_id}/update")
.post(body) .post(body)
.build() .build()
return authClient.newCall(request) return authClient.newCall(request)
.asObservableSuccess() .asObservableSuccess()
.map { .map {
track track
} }
} }
fun updateLibManga(track: Track): Observable<Track> { fun updateLibManga(track: Track): Observable<Track> {
// chapter update // chapter update
val body = FormBody.Builder() val body = FormBody.Builder()
.add("watched_eps", track.last_chapter_read.toString()) .add("watched_eps", track.last_chapter_read.toString())
.build() .build()
val request = Request.Builder() val request = Request.Builder()
.url("$apiUrl/subject/${track.media_id}/update/watched_eps") .url("$apiUrl/subject/${track.media_id}/update/watched_eps")
.post(body) .post(body)
.build() .build()
// read status update // read status update
val sbody = FormBody.Builder() val sbody = FormBody.Builder()
.add("status", track.toBangumiStatus()) .add("status", track.toBangumiStatus())
.build() .build()
val srequest = Request.Builder() val srequest = Request.Builder()
.url("$apiUrl/collection/${track.media_id}/update") .url("$apiUrl/collection/${track.media_id}/update")
.post(sbody) .post(sbody)
.build() .build()
return authClient.newCall(srequest) return authClient.newCall(srequest)
.asObservableSuccess() .asObservableSuccess()
.map { .map {
track track
}.flatMap { }.flatMap {
authClient.newCall(request) authClient.newCall(request)
.asObservableSuccess() .asObservableSuccess()
.map { .map {
track track
} }
} }
} }
fun search(search: String): Observable<List<TrackSearch>> { fun search(search: String): Observable<List<TrackSearch>> {
val url = Uri.parse( val url = Uri.parse(
"$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}").buildUpon() "$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}"
.appendQueryParameter("max_results", "20") ).buildUpon()
.build() .appendQueryParameter("max_results", "20")
.build()
val request = Request.Builder() val request = Request.Builder()
.url(url.toString()) .url(url.toString())
.get() .get()
.build() .build()
return authClient.newCall(request) return authClient.newCall(request)
.asObservableSuccess() .asObservableSuccess()
.map { netResponse -> .map { netResponse ->
var responseBody = netResponse.body?.string().orEmpty() var responseBody = netResponse.body?.string().orEmpty()
if (responseBody.isEmpty()) { if (responseBody.isEmpty()) {
throw Exception("Null Response") 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) }
} }
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 { private fun jsonToSearch(obj: JsonObject): TrackSearch {
@ -109,9 +110,15 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
return Track.create(TrackManager.BANGUMI).apply { return Track.create(TrackManager.BANGUMI).apply {
title = mangas["name"].asString title = mangas["name"].asString
media_id = mangas["id"].asInt media_id = mangas["id"].asInt
score = if (mangas["rating"] != null) score = if (mangas["rating"] != null) {
(if (mangas["rating"].isJsonObject) mangas["rating"].obj["score"].asFloat else 0f) if (mangas["rating"].isJsonObject) {
else 0f mangas["rating"].obj["score"].asFloat
} else {
0f
}
} else {
0f
}
status = Bangumi.DEFAULT_STATUS status = Bangumi.DEFAULT_STATUS
tracking_url = mangas["url"].asString tracking_url = mangas["url"].asString
} }
@ -120,37 +127,37 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
fun findLibManga(track: Track): Observable<Track?> { fun findLibManga(track: Track): Observable<Track?> {
val urlMangas = "$apiUrl/subject/${track.media_id}" val urlMangas = "$apiUrl/subject/${track.media_id}"
val requestMangas = Request.Builder() val requestMangas = Request.Builder()
.url(urlMangas) .url(urlMangas)
.get() .get()
.build() .build()
return authClient.newCall(requestMangas) return authClient.newCall(requestMangas)
.asObservableSuccess() .asObservableSuccess()
.map { netResponse -> .map { netResponse ->
// get comic info // get comic info
val responseBody = netResponse.body?.string().orEmpty() val responseBody = netResponse.body?.string().orEmpty()
jsonToTrack(JsonParser.parseString(responseBody).obj) jsonToTrack(JsonParser.parseString(responseBody).obj)
} }
} }
fun statusLibManga(track: Track): Observable<Track?> { fun statusLibManga(track: Track): Observable<Track?> {
val urlUserRead = "$apiUrl/collection/${track.media_id}" val urlUserRead = "$apiUrl/collection/${track.media_id}"
val requestUserRead = Request.Builder() val requestUserRead = Request.Builder()
.url(urlUserRead) .url(urlUserRead)
.cacheControl(CacheControl.FORCE_NETWORK) .cacheControl(CacheControl.FORCE_NETWORK)
.get() .get()
.build() .build()
// todo get user readed chapter here // todo get user readed chapter here
return authClient.newCall(requestUserRead) return authClient.newCall(requestUserRead)
.asObservableSuccess() .asObservableSuccess()
.map { netResponse -> .map { netResponse ->
val resp = netResponse.body?.string() val resp = netResponse.body?.string()
val coll = gson.fromJson(resp, Collection::class.java) val coll = gson.fromJson(resp, Collection::class.java)
track.status = coll.status?.id!! track.status = coll.status?.id!!
track.last_chapter_read = coll.ep_status!! track.last_chapter_read = coll.ep_status!!
track track
} }
} }
fun accessToken(code: String): Observable<OAuth> { 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, private fun accessTokenRequest(code: String) = POST(
body = FormBody.Builder() oauthUrl,
.add("grant_type", "authorization_code") body = FormBody.Builder()
.add("client_id", clientId) .add("grant_type", "authorization_code")
.add("client_secret", clientSecret) .add("client_id", clientId)
.add("code", code) .add("client_secret", clientSecret)
.add("redirect_uri", redirectUrl) .add("code", code)
.build() .add("redirect_uri", redirectUrl)
.build()
) )
companion object { companion object {
@ -190,19 +198,21 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
} }
fun authUrl() = fun authUrl() =
Uri.parse(loginUrl).buildUpon() Uri.parse(loginUrl).buildUpon()
.appendQueryParameter("client_id", clientId) .appendQueryParameter("client_id", clientId)
.appendQueryParameter("response_type", "code") .appendQueryParameter("response_type", "code")
.appendQueryParameter("redirect_uri", redirectUrl) .appendQueryParameter("redirect_uri", redirectUrl)
.build() .build()
fun refreshTokenRequest(token: String) = POST(oauthUrl, fun refreshTokenRequest(token: String) = POST(
body = FormBody.Builder() oauthUrl,
.add("grant_type", "refresh_token") body = FormBody.Builder()
.add("client_id", clientId) .add("grant_type", "refresh_token")
.add("client_secret", clientSecret) .add("client_id", clientId)
.add("refresh_token", token) .add("client_secret", clientSecret)
.add("redirect_uri", redirectUrl) .add("refresh_token", token)
.build()) .add("redirect_uri", redirectUrl)
.build()
)
} }
} }

View File

@ -36,25 +36,28 @@ class BangumiInterceptor(val bangumi: Bangumi, val gson: Gson) : Interceptor {
} }
val authRequest = if (originalRequest.method == "GET") originalRequest.newBuilder() val authRequest = if (originalRequest.method == "GET") originalRequest.newBuilder()
.header("User-Agent", "Tachiyomi") .header("User-Agent", "Tachiyomi")
.url(originalRequest.url.newBuilder() .url(
.addQueryParameter("access_token", currAuth.access_token).build()) originalRequest.url.newBuilder()
.build() else originalRequest.newBuilder() .addQueryParameter("access_token", currAuth.access_token).build()
.post(addTocken(currAuth.access_token, originalRequest.body as FormBody)) )
.header("User-Agent", "Tachiyomi") .build() else originalRequest.newBuilder()
.build() .post(addTocken(currAuth.access_token, originalRequest.body as FormBody))
.header("User-Agent", "Tachiyomi")
.build()
return chain.proceed(authRequest) return chain.proceed(authRequest)
} }
fun newAuth(oauth: OAuth?) { fun newAuth(oauth: OAuth?) {
this.oauth = if (oauth == null) null else OAuth( this.oauth = if (oauth == null) null else OAuth(
oauth.access_token, oauth.access_token,
oauth.token_type, oauth.token_type,
System.currentTimeMillis() / 1000, System.currentTimeMillis() / 1000,
oauth.expires_in, oauth.expires_in,
oauth.refresh_token, oauth.refresh_token,
this.oauth?.user_id) this.oauth?.user_id
)
bangumi.saveToken(oauth) bangumi.saveToken(oauth)
} }

View File

@ -78,17 +78,17 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
override fun bind(track: Track): Observable<Track> { override fun bind(track: Track): Observable<Track> {
return api.findLibManga(track, getUserId()) return api.findLibManga(track, getUserId())
.flatMap { remoteTrack -> .flatMap { remoteTrack ->
if (remoteTrack != null) { if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack) track.copyPersonalFrom(remoteTrack)
track.media_id = remoteTrack.media_id track.media_id = remoteTrack.media_id
update(track) update(track)
} else { } else {
track.score = DEFAULT_SCORE track.score = DEFAULT_SCORE
track.status = DEFAULT_STATUS track.status = DEFAULT_STATUS
add(track) add(track)
}
} }
}
} }
override fun search(query: String): Observable<List<TrackSearch>> { 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> { override fun refresh(track: Track): Observable<Track> {
return api.getLibManga(track) return api.getLibManga(track)
.map { remoteTrack -> .map { remoteTrack ->
track.copyPersonalFrom(remoteTrack) track.copyPersonalFrom(remoteTrack)
track.total_chapters = remoteTrack.total_chapters track.total_chapters = remoteTrack.total_chapters
track track
} }
} }
override fun login(username: String, password: String): Completable { override fun login(username: String, password: String): Completable {
return api.login(username, password) return api.login(username, password)
.doOnNext { interceptor.newAuth(it) } .doOnNext { interceptor.newAuth(it) }
.flatMap { api.getCurrentUser() } .flatMap { api.getCurrentUser() }
.doOnNext { userId -> saveCredentials(username, userId) } .doOnNext { userId -> saveCredentials(username, userId) }
.doOnError { logout() } .doOnError { logout() }
.toCompletable() .toCompletable()
} }
override fun logout() { override fun logout() {

View File

@ -33,59 +33,59 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
private val authClient = client.newBuilder().addInterceptor(interceptor).build() private val authClient = client.newBuilder().addInterceptor(interceptor).build()
private val rest = Retrofit.Builder() private val rest = Retrofit.Builder()
.baseUrl(baseUrl) .baseUrl(baseUrl)
.client(authClient) .client(authClient)
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().serializeNulls().create())) .addConverterFactory(GsonConverterFactory.create(GsonBuilder().serializeNulls().create()))
.addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build() .build()
.create(Rest::class.java) .create(Rest::class.java)
private val searchRest = Retrofit.Builder() private val searchRest = Retrofit.Builder()
.baseUrl(algoliaKeyUrl) .baseUrl(algoliaKeyUrl)
.client(authClient) .client(authClient)
.addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build() .build()
.create(SearchKeyRest::class.java) .create(SearchKeyRest::class.java)
private val algoliaRest = Retrofit.Builder() private val algoliaRest = Retrofit.Builder()
.baseUrl(algoliaUrl) .baseUrl(algoliaUrl)
.client(client) .client(client)
.addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build() .build()
.create(AgoliaSearchRest::class.java) .create(AgoliaSearchRest::class.java)
fun addLibManga(track: Track, userId: String): Observable<Track> { fun addLibManga(track: Track, userId: String): Observable<Track> {
return Observable.defer { return Observable.defer {
// @formatter:off // @formatter:off
val data = jsonObject( val data = jsonObject(
"type" to "libraryEntries", "type" to "libraryEntries",
"attributes" to jsonObject( "attributes" to jsonObject(
"status" to track.toKitsuStatus(), "status" to track.toKitsuStatus(),
"progress" to track.last_chapter_read "progress" to track.last_chapter_read
),
"relationships" to jsonObject(
"user" to jsonObject(
"data" to jsonObject(
"id" to userId,
"type" to "users"
)
), ),
"relationships" to jsonObject( "media" to jsonObject(
"user" to jsonObject( "data" to jsonObject(
"data" to jsonObject( "id" to track.media_id,
"id" to userId, "type" to "manga"
"type" to "users" )
)
),
"media" to jsonObject(
"data" to jsonObject(
"id" to track.media_id,
"type" to "manga"
)
)
) )
)
) )
rest.addLibManga(jsonObject("data" to data)) rest.addLibManga(jsonObject("data" to data))
.map { json -> .map { json ->
track.media_id = json["data"]["id"].int track.media_id = json["data"]["id"].int
track track
} }
} }
} }
@ -93,77 +93,77 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
return Observable.defer { return Observable.defer {
// @formatter:off // @formatter:off
val data = jsonObject( val data = jsonObject(
"type" to "libraryEntries", "type" to "libraryEntries",
"id" to track.media_id, "id" to track.media_id,
"attributes" to jsonObject( "attributes" to jsonObject(
"status" to track.toKitsuStatus(), "status" to track.toKitsuStatus(),
"progress" to track.last_chapter_read, "progress" to track.last_chapter_read,
"ratingTwenty" to track.toKitsuScore() "ratingTwenty" to track.toKitsuScore()
) )
) )
// @formatter:on // @formatter:on
rest.updateLibManga(track.media_id, jsonObject("data" to data)) rest.updateLibManga(track.media_id, jsonObject("data" to data))
.map { track } .map { track }
} }
} }
fun search(query: String): Observable<List<TrackSearch>> { fun search(query: String): Observable<List<TrackSearch>> {
return searchRest return searchRest
.getKey().map { json -> .getKey().map { json ->
json["media"].asJsonObject["key"].string json["media"].asJsonObject["key"].string
}.flatMap { key -> }.flatMap { key ->
algoliaSearch(key, query) algoliaSearch(key, query)
} }
} }
private fun algoliaSearch(key: String, query: String): Observable<List<TrackSearch>> { private fun algoliaSearch(key: String, query: String): Observable<List<TrackSearch>> {
val jsonObject = jsonObject("params" to "query=$query$algoliaFilter") val jsonObject = jsonObject("params" to "query=$query$algoliaFilter")
return algoliaRest return algoliaRest
.getSearchQuery(algoliaAppId, key, jsonObject) .getSearchQuery(algoliaAppId, key, jsonObject)
.map { json -> .map { json ->
val data = json["hits"].array val data = json["hits"].array
data.map { KitsuSearchManga(it.obj) } data.map { KitsuSearchManga(it.obj) }
.filter { it.subType != "novel" } .filter { it.subType != "novel" }
.map { it.toTrack() } .map { it.toTrack() }
} }
} }
fun findLibManga(track: Track, userId: String): Observable<Track?> { fun findLibManga(track: Track, userId: String): Observable<Track?> {
return rest.findLibManga(track.media_id, userId) return rest.findLibManga(track.media_id, userId)
.map { json -> .map { json ->
val data = json["data"].array val data = json["data"].array
if (data.size() > 0) { if (data.size() > 0) {
val manga = json["included"].array[0].obj val manga = json["included"].array[0].obj
KitsuLibManga(data[0].obj, manga).toTrack() KitsuLibManga(data[0].obj, manga).toTrack()
} else { } else {
null null
}
} }
}
} }
fun getLibManga(track: Track): Observable<Track> { fun getLibManga(track: Track): Observable<Track> {
return rest.getLibManga(track.media_id) return rest.getLibManga(track.media_id)
.map { json -> .map { json ->
val data = json["data"].array val data = json["data"].array
if (data.size() > 0) { if (data.size() > 0) {
val manga = json["included"].array[0].obj val manga = json["included"].array[0].obj
KitsuLibManga(data[0].obj, manga).toTrack() KitsuLibManga(data[0].obj, manga).toTrack()
} else { } else {
throw Exception("Could not find manga") throw Exception("Could not find manga")
}
} }
}
} }
fun login(username: String, password: String): Observable<OAuth> { fun login(username: String, password: String): Observable<OAuth> {
return Retrofit.Builder() return Retrofit.Builder()
.baseUrl(loginUrl) .baseUrl(loginUrl)
.client(client) .client(client)
.addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build() .build()
.create(LoginRest::class.java) .create(LoginRest::class.java)
.requestAccessToken(username, password) .requestAccessToken(username, password)
} }
fun getCurrentUser(): Observable<String> { fun getCurrentUser(): Observable<String> {
@ -242,12 +242,14 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
return baseMangaUrl + remoteId return baseMangaUrl + remoteId
} }
fun refreshTokenRequest(token: String) = POST("${loginUrl}oauth/token", fun refreshTokenRequest(token: String) = POST(
body = FormBody.Builder() "${loginUrl}oauth/token",
.add("grant_type", "refresh_token") body = FormBody.Builder()
.add("client_id", clientId) .add("grant_type", "refresh_token")
.add("client_secret", clientSecret) .add("client_id", clientId)
.add("refresh_token", token) .add("client_secret", clientSecret)
.build()) .add("refresh_token", token)
.build()
)
} }
} }

View File

@ -30,10 +30,10 @@ class KitsuInterceptor(val kitsu: Kitsu, val gson: Gson) : Interceptor {
// Add the authorization header to the original request. // Add the authorization header to the original request.
val authRequest = originalRequest.newBuilder() val authRequest = originalRequest.newBuilder()
.addHeader("Authorization", "Bearer ${oauth!!.access_token}") .addHeader("Authorization", "Bearer ${oauth!!.access_token}")
.header("Accept", "application/vnd.api+json") .header("Accept", "application/vnd.api+json")
.header("Content-Type", "application/vnd.api+json") .header("Content-Type", "application/vnd.api+json")
.build() .build()
return chain.proceed(authRequest) return chain.proceed(authRequest)
} }

View File

@ -74,17 +74,17 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
override fun bind(track: Track): Observable<Track> { override fun bind(track: Track): Observable<Track> {
return api.findLibManga(track) return api.findLibManga(track)
.flatMap { remoteTrack -> .flatMap { remoteTrack ->
if (remoteTrack != null) { if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack) track.copyPersonalFrom(remoteTrack)
update(track) update(track)
} else { } else {
// Set default fields if it's not found in the list // Set default fields if it's not found in the list
track.score = DEFAULT_SCORE.toFloat() track.score = DEFAULT_SCORE.toFloat()
track.status = DEFAULT_STATUS track.status = DEFAULT_STATUS
add(track) add(track)
}
} }
}
} }
override fun search(query: String): Observable<List<TrackSearch>> { 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> { override fun refresh(track: Track): Observable<Track> {
return api.getLibManga(track) return api.getLibManga(track)
.map { remoteTrack -> .map { remoteTrack ->
track.copyPersonalFrom(remoteTrack) track.copyPersonalFrom(remoteTrack)
track.total_chapters = remoteTrack.total_chapters track.total_chapters = remoteTrack.total_chapters
track track
} }
} }
override fun login(username: String, password: String): Completable { override fun login(username: String, password: String): Completable {
logout() logout()
return Observable.fromCallable { api.login(username, password) } return Observable.fromCallable { api.login(username, password) }
.doOnNext { csrf -> saveCSRF(csrf) } .doOnNext { csrf -> saveCSRF(csrf) }
.doOnNext { saveCredentials(username, password) } .doOnNext { saveCredentials(username, password) }
.doOnError { logout() } .doOnError { logout() }
.toCompletable() .toCompletable()
} }
fun refreshLogin() { fun refreshLogin() {
@ -141,8 +141,8 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
val isAuthorized: Boolean val isAuthorized: Boolean
get() = super.isLogged && get() = super.isLogged &&
getCSRF().isNotEmpty() && getCSRF().isNotEmpty() &&
checkCookies() checkCookies()
fun getCSRF(): String = preferences.trackToken(this).get() fun getCSRF(): String = preferences.trackToken(this).get()
@ -152,8 +152,9 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
var ckCount = 0 var ckCount = 0
val url = BASE_URL.toHttpUrlOrNull()!! val url = BASE_URL.toHttpUrlOrNull()!!
for (ck in networkService.cookieManager.get(url)) { 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++ ckCount++
}
} }
return ckCount == 2 return ckCount == 2

View File

@ -39,43 +39,45 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
return if (query.startsWith(PREFIX_MY)) { return if (query.startsWith(PREFIX_MY)) {
val realQuery = query.removePrefix(PREFIX_MY) val realQuery = query.removePrefix(PREFIX_MY)
getList() getList()
.flatMap { Observable.from(it) } .flatMap { Observable.from(it) }
.filter { it.title.contains(realQuery, true) } .filter { it.title.contains(realQuery, true) }
.toList() .toList()
} else { } else {
client.newCall(GET(searchUrl(query))) client.newCall(GET(searchUrl(query)))
.asObservable() .asObservable()
.flatMap { response -> .flatMap { response ->
Observable.from(Jsoup.parse(response.consumeBody()) Observable.from(
.select("div.js-categories-seasonal.js-block-list.list") Jsoup.parse(response.consumeBody())
.select("table").select("tbody") .select("div.js-categories-seasonal.js-block-list.list")
.select("tr").drop(1)) .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" .toList()
}
.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()
} }
} }
fun addLibManga(track: Track): Observable<Track> { fun addLibManga(track: Track): Observable<Track> {
return Observable.defer { return Observable.defer {
authClient.newCall(POST(url = addUrl(), body = mangaPostPayload(track))) authClient.newCall(POST(url = addUrl(), body = mangaPostPayload(track)))
.asObservableSuccess() .asObservableSuccess()
.map { track } .map { track }
} }
} }
@ -95,40 +97,40 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
// Update remote // Update remote
authClient.newCall(POST(url = editPageUrl(track.media_id), body = mangaEditPostBody(editData))) authClient.newCall(POST(url = editPageUrl(track.media_id), body = mangaEditPostBody(editData)))
.asObservableSuccess() .asObservableSuccess()
.map { .map {
track track
} }
} }
} }
fun findLibManga(track: Track): Observable<Track?> { fun findLibManga(track: Track): Observable<Track?> {
return authClient.newCall(GET(url = editPageUrl(track.media_id))) return authClient.newCall(GET(url = editPageUrl(track.media_id)))
.asObservable() .asObservable()
.map { response -> .map { response ->
var libTrack: Track? = null var libTrack: Track? = null
response.use { response.use {
if (it.priorResponse?.isRedirect != true) { if (it.priorResponse?.isRedirect != true) {
val trackForm = Jsoup.parse(it.consumeBody()) val trackForm = Jsoup.parse(it.consumeBody())
libTrack = Track.create(TrackManager.MYANIMELIST).apply { libTrack = Track.create(TrackManager.MYANIMELIST).apply {
last_chapter_read = trackForm.select("#add_manga_num_read_chapters").`val`().toInt() last_chapter_read = trackForm.select("#add_manga_num_read_chapters").`val`().toInt()
total_chapters = trackForm.select("#totalChap").text().toInt() total_chapters = trackForm.select("#totalChap").text().toInt()
status = trackForm.select("#add_manga_status > option[selected]").`val`().toInt() status = trackForm.select("#add_manga_status > option[selected]").`val`().toInt()
score = trackForm.select("#add_manga_score > option[selected]").`val`().toFloatOrNull() score = trackForm.select("#add_manga_score > option[selected]").`val`().toFloatOrNull()
?: 0f ?: 0f
started_reading_date = trackForm.searchDatePicker("#add_manga_start_date") started_reading_date = trackForm.searchDatePicker("#add_manga_start_date")
finished_reading_date = trackForm.searchDatePicker("#add_manga_finish_date") finished_reading_date = trackForm.searchDatePicker("#add_manga_finish_date")
}
} }
} }
libTrack
} }
libTrack
}
} }
fun getLibManga(track: Track): Observable<Track> { fun getLibManga(track: Track): Observable<Track> {
return findLibManga(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 { 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() val response = client.newCall(GET(loginUrl())).execute()
return Jsoup.parse(response.consumeBody()) return Jsoup.parse(response.consumeBody())
.select("meta[name=csrf_token]") .select("meta[name=csrf_token]")
.attr("content") .attr("content")
} }
private fun login(username: String, password: String, csrf: String) { 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>> { private fun getList(): Observable<List<TrackSearch>> {
return getListUrl() return getListUrl()
.flatMap { url -> .flatMap { url ->
getListXml(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")) .toList()
}
.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()
} }
private fun getListUrl(): Observable<String> { private fun getListUrl(): Observable<String> {
return authClient.newCall(POST(url = exportListUrl(), body = exportPostBody())) return authClient.newCall(POST(url = exportListUrl(), body = exportPostBody()))
.asObservable() .asObservable()
.map { response -> .map { response ->
baseUrl + Jsoup.parse(response.consumeBody()) baseUrl + Jsoup.parse(response.consumeBody())
.select("div.goodresult") .select("div.goodresult")
.select("a") .select("a")
.attr("href") .attr("href")
} }
} }
private fun getListXml(url: String): Observable<Document> { private fun getListXml(url: String): Observable<Document> {
return authClient.newCall(GET(url)) return authClient.newCall(GET(url))
.asObservable() .asObservable()
.map { response -> .map { response ->
Jsoup.parse(response.consumeXmlBody(), "", Parser.xmlParser()) Jsoup.parse(response.consumeXmlBody(), "", Parser.xmlParser())
} }
} }
private fun Response.consumeBody(): String? { 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") val tables = page.select("form#main-form table")
return MyAnimeListEditData( return MyAnimeListEditData(
entry_id = tables[0].select("input[name=entry_id]").`val`(), // Always 0 entry_id = tables[0].select("input[name=entry_id]").`val`(), // Always 0
manga_id = tables[0].select("#manga_id").`val`(), manga_id = tables[0].select("#manga_id").`val`(),
status = tables[0].select("#add_manga_status > option[selected]").`val`(), status = tables[0].select("#add_manga_status > option[selected]").`val`(),
num_read_volumes = tables[0].select("#add_manga_num_read_volumes").`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 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`(), num_read_chapters = tables[0].select("#add_manga_num_read_chapters").`val`(),
score = tables[0].select("#add_manga_score > option[selected]").`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_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_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`(), 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_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_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`(), finish_date_year = tables[0].select("#add_manga_finish_date_year > option[selected]").`val`(),
tags = tables[1].select("#add_manga_tags").`val`(), tags = tables[1].select("#add_manga_tags").`val`(),
priority = tables[1].select("#add_manga_priority > option[selected]").`val`(), priority = tables[1].select("#add_manga_priority > option[selected]").`val`(),
storage_type = tables[1].select("#add_manga_storage_type > 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_retail_volumes = tables[1].select("#add_manga_num_retail_volumes").`val`(),
num_read_times = tables[1].select("#add_manga_num_read_times").`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`(), reread_value = tables[1].select("#add_manga_reread_value > option[selected]").`val`(),
comments = tables[1].select("#add_manga_comments").`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`(), 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`() 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 mangaUrl(remoteId: Int) = baseMangaUrl + remoteId
private fun loginUrl() = Uri.parse(baseUrl).buildUpon() private fun loginUrl() = Uri.parse(baseUrl).buildUpon()
.appendPath("login.php") .appendPath("login.php")
.toString() .toString()
private fun searchUrl(query: String): String { private fun searchUrl(query: String): String {
val col = "c[]" val col = "c[]"
return Uri.parse(baseUrl).buildUpon() return Uri.parse(baseUrl).buildUpon()
.appendPath("manga.php") .appendPath("manga.php")
.appendQueryParameter("q", query) .appendQueryParameter("q", query)
.appendQueryParameter(col, "a") .appendQueryParameter(col, "a")
.appendQueryParameter(col, "b") .appendQueryParameter(col, "b")
.appendQueryParameter(col, "c") .appendQueryParameter(col, "c")
.appendQueryParameter(col, "d") .appendQueryParameter(col, "d")
.appendQueryParameter(col, "e") .appendQueryParameter(col, "e")
.appendQueryParameter(col, "g") .appendQueryParameter(col, "g")
.toString() .toString()
} }
private fun exportListUrl() = Uri.parse(baseUrl).buildUpon() private fun exportListUrl() = Uri.parse(baseUrl).buildUpon()
.appendPath("panel.php") .appendPath("panel.php")
.appendQueryParameter("go", "export") .appendQueryParameter("go", "export")
.toString() .toString()
private fun editPageUrl(mediaId: Int) = Uri.parse(baseModifyListUrl).buildUpon() private fun editPageUrl(mediaId: Int) = Uri.parse(baseModifyListUrl).buildUpon()
.appendPath(mediaId.toString()) .appendPath(mediaId.toString())
.appendPath("edit") .appendPath("edit")
.toString() .toString()
private fun addUrl() = Uri.parse(baseModifyListUrl).buildUpon() private fun addUrl() = Uri.parse(baseModifyListUrl).buildUpon()
.appendPath("add.json") .appendPath("add.json")
.toString() .toString()
private fun loginPostBody(username: String, password: String, csrf: String): RequestBody { private fun loginPostBody(username: String, password: String, csrf: String): RequestBody {
return FormBody.Builder() return FormBody.Builder()
.add("user_name", username) .add("user_name", username)
.add("password", password) .add("password", password)
.add("cookie", "1") .add("cookie", "1")
.add("sublogin", "Login") .add("sublogin", "Login")
.add("submit", "1") .add("submit", "1")
.add(CSRF, csrf) .add(CSRF, csrf)
.build() .build()
} }
private fun exportPostBody(): RequestBody { private fun exportPostBody(): RequestBody {
return FormBody.Builder() return FormBody.Builder()
.add("type", "2") .add("type", "2")
.add("subexport", "Export My List") .add("subexport", "Export My List")
.build() .build()
} }
private fun mangaPostPayload(track: Track): RequestBody { private fun mangaPostPayload(track: Track): RequestBody {
val body = JSONObject() val body = JSONObject()
.put("manga_id", track.media_id) .put("manga_id", track.media_id)
.put("status", track.status) .put("status", track.status)
.put("score", track.score) .put("score", track.score)
.put("num_read_chapters", track.last_chapter_read) .put("num_read_chapters", track.last_chapter_read)
return body.toString().toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull()) return body.toString().toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
} }
private fun mangaEditPostBody(track: MyAnimeListEditData): RequestBody { private fun mangaEditPostBody(track: MyAnimeListEditData): RequestBody {
return FormBody.Builder() return FormBody.Builder()
.add("entry_id", track.entry_id) .add("entry_id", track.entry_id)
.add("manga_id", track.manga_id) .add("manga_id", track.manga_id)
.add("add_manga[status]", track.status) .add("add_manga[status]", track.status)
.add("add_manga[num_read_volumes]", track.num_read_volumes) .add("add_manga[num_read_volumes]", track.num_read_volumes)
.add("last_completed_vol", track.last_completed_vol) .add("last_completed_vol", track.last_completed_vol)
.add("add_manga[num_read_chapters]", track.num_read_chapters) .add("add_manga[num_read_chapters]", track.num_read_chapters)
.add("add_manga[score]", track.score) .add("add_manga[score]", track.score)
.add("add_manga[start_date][month]", track.start_date_month) .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][day]", track.start_date_day)
.add("add_manga[start_date][year]", track.start_date_year) .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][month]", track.finish_date_month)
.add("add_manga[finish_date][day]", track.finish_date_day) .add("add_manga[finish_date][day]", track.finish_date_day)
.add("add_manga[finish_date][year]", track.finish_date_year) .add("add_manga[finish_date][year]", track.finish_date_year)
.add("add_manga[tags]", track.tags) .add("add_manga[tags]", track.tags)
.add("add_manga[priority]", track.priority) .add("add_manga[priority]", track.priority)
.add("add_manga[storage_type]", track.storage_type) .add("add_manga[storage_type]", track.storage_type)
.add("add_manga[num_retail_volumes]", track.num_retail_volumes) .add("add_manga[num_retail_volumes]", track.num_retail_volumes)
.add("add_manga[num_read_times]", track.num_read_chapters) .add("add_manga[num_read_times]", track.num_read_chapters)
.add("add_manga[reread_value]", track.reread_value) .add("add_manga[reread_value]", track.reread_value)
.add("add_manga[comments]", track.comments) .add("add_manga[comments]", track.comments)
.add("add_manga[is_asked_to_discuss]", track.is_asked_to_discuss) .add("add_manga[is_asked_to_discuss]", track.is_asked_to_discuss)
.add("add_manga[sns_post_type]", track.sns_post_type) .add("add_manga[sns_post_type]", track.sns_post_type)
.add("submitIt", track.submitIt) .add("submitIt", track.submitIt)
.build() .build()
} }
private fun Element.searchDateXml(field: String): Long { private fun Element.searchDateXml(field: String): Long {
val text = selectText(field, "0000-00-00")!! val text = selectText(field, "0000-00-00")!!
// MAL sets the data to 0000-00-00 when date is invalid or missing // 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 0L
}
return SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(text)?.time ?: 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 month = select(id + "_month > option[selected]").`val`().toIntOrNull()
val day = select(id + "_day > option[selected]").`val`().toIntOrNull() val day = select(id + "_day > option[selected]").`val`().toIntOrNull()
val year = select(id + "_year > 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 0L
}
return GregorianCalendar(year, month - 1, day).timeInMillis 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.searchTotalChapters() = if (select(TD)[4].text() == "-") 0 else select(TD)[4].text().toInt()
private fun Element.searchCoverUrl() = select("img") private fun Element.searchCoverUrl() = select("img")
.attr("data-src") .attr("data-src")
.split("\\?")[0] .split("\\?")[0]
.replace("/r/50x70/", "/") .replace("/r/50x70/", "/")
private fun Element.searchMediaId() = select("div.picSurround") private fun Element.searchMediaId() = select("div.picSurround")
.select("a").attr("id") .select("a").attr("id")
.replace("sarea", "") .replace("sarea", "")
.toInt() .toInt()
private fun Element.searchSummary() = select("div.pt4") private fun Element.searchSummary() = select("div.pt4")
.first() .first()
.ownText()!! .ownText()!!
private fun Element.searchPublishingStatus() = if (select(TD).last().text() == "-") "Publishing" else "Finished" 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) { fun copyPersonalFrom(track: Track) {
num_read_chapters = track.last_chapter_read.toString() num_read_chapters = track.last_chapter_read.toString()
val numScore = track.score.toInt() val numScore = track.score.toInt()
if (numScore in 1..9) if (numScore in 1..9) {
score = numScore.toString() score = numScore.toString()
}
status = track.status.toString() status = track.status.toString()
if (track.started_reading_date == 0L) { if (track.started_reading_date == 0L) {
start_date_month = "" start_date_month = ""

View File

@ -53,7 +53,7 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor
private fun updateJsonBody(requestBody: RequestBody): RequestBody { private fun updateJsonBody(requestBody: RequestBody): RequestBody {
val jsonString = bodyToString(requestBody) val jsonString = bodyToString(requestBody)
val newBody = JSONObject(jsonString) val newBody = JSONObject(jsonString)
.put(MyAnimeListApi.CSRF, myanimelist.getCSRF()) .put(MyAnimeListApi.CSRF, myanimelist.getCSRF())
return newBody.toString().toRequestBody(requestBody.contentType()) return newBody.toString().toRequestBody(requestBody.contentType())
} }

View File

@ -51,18 +51,18 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
override fun bind(track: Track): Observable<Track> { override fun bind(track: Track): Observable<Track> {
return api.findLibManga(track, getUsername()) return api.findLibManga(track, getUsername())
.flatMap { remoteTrack -> .flatMap { remoteTrack ->
if (remoteTrack != null) { if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack) track.copyPersonalFrom(remoteTrack)
track.library_id = remoteTrack.library_id track.library_id = remoteTrack.library_id
update(track) update(track)
} else { } else {
// Set default fields if it's not found in the list // Set default fields if it's not found in the list
track.score = DEFAULT_SCORE.toFloat() track.score = DEFAULT_SCORE.toFloat()
track.status = DEFAULT_STATUS track.status = DEFAULT_STATUS
add(track) add(track)
}
} }
}
} }
override fun search(query: String): Observable<List<TrackSearch>> { 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> { override fun refresh(track: Track): Observable<Track> {
return api.findLibManga(track, getUsername()) return api.findLibManga(track, getUsername())
.map { remoteTrack -> .map { remoteTrack ->
if (remoteTrack != null) { if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack) track.copyPersonalFrom(remoteTrack)
track.total_chapters = remoteTrack.total_chapters track.total_chapters = remoteTrack.total_chapters
}
track
} }
track
}
} }
override fun getLogo() = R.drawable.ic_tracker_shikimori override fun getLogo() = R.drawable.ic_tracker_shikimori

View File

@ -30,49 +30,49 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
fun addLibManga(track: Track, user_id: String): Observable<Track> { fun addLibManga(track: Track, user_id: String): Observable<Track> {
val payload = jsonObject( val payload = jsonObject(
"user_rate" to jsonObject( "user_rate" to jsonObject(
"user_id" to user_id, "user_id" to user_id,
"target_id" to track.media_id, "target_id" to track.media_id,
"target_type" to "Manga", "target_type" to "Manga",
"chapters" to track.last_chapter_read, "chapters" to track.last_chapter_read,
"score" to track.score.toInt(), "score" to track.score.toInt(),
"status" to track.toShikimoriStatus() "status" to track.toShikimoriStatus()
) )
) )
val body = payload.toString().toRequestBody(jsonime) val body = payload.toString().toRequestBody(jsonime)
val request = Request.Builder() val request = Request.Builder()
.url("$apiUrl/v2/user_rates") .url("$apiUrl/v2/user_rates")
.post(body) .post(body)
.build() .build()
return authClient.newCall(request) return authClient.newCall(request)
.asObservableSuccess() .asObservableSuccess()
.map { .map {
track track
} }
} }
fun updateLibManga(track: Track, user_id: String): Observable<Track> = addLibManga(track, user_id) fun updateLibManga(track: Track, user_id: String): Observable<Track> = addLibManga(track, user_id)
fun search(search: String): Observable<List<TrackSearch>> { fun search(search: String): Observable<List<TrackSearch>> {
val url = Uri.parse("$apiUrl/mangas").buildUpon() val url = Uri.parse("$apiUrl/mangas").buildUpon()
.appendQueryParameter("order", "popularity") .appendQueryParameter("order", "popularity")
.appendQueryParameter("search", search) .appendQueryParameter("search", search)
.appendQueryParameter("limit", "20") .appendQueryParameter("limit", "20")
.build() .build()
val request = Request.Builder() val request = Request.Builder()
.url(url.toString()) .url(url.toString())
.get() .get()
.build() .build()
return authClient.newCall(request) return authClient.newCall(request)
.asObservableSuccess() .asObservableSuccess()
.map { netResponse -> .map { netResponse ->
val responseBody = netResponse.body?.string().orEmpty() val responseBody = netResponse.body?.string().orEmpty()
if (responseBody.isEmpty()) { if (responseBody.isEmpty()) {
throw Exception("Null Response") throw Exception("Null Response")
}
val response = JsonParser.parseString(responseBody).array
response.map { jsonToSearch(it.obj) }
} }
val response = JsonParser.parseString(responseBody).array
response.map { jsonToSearch(it.obj) }
}
} }
private fun jsonToSearch(obj: JsonObject): TrackSearch { 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?> { fun findLibManga(track: Track, user_id: String): Observable<Track?> {
val url = Uri.parse("$apiUrl/v2/user_rates").buildUpon() val url = Uri.parse("$apiUrl/v2/user_rates").buildUpon()
.appendQueryParameter("user_id", user_id) .appendQueryParameter("user_id", user_id)
.appendQueryParameter("target_id", track.media_id.toString()) .appendQueryParameter("target_id", track.media_id.toString())
.appendQueryParameter("target_type", "Manga") .appendQueryParameter("target_type", "Manga")
.build() .build()
val request = Request.Builder() val request = Request.Builder()
.url(url.toString()) .url(url.toString())
.get() .get()
.build() .build()
val urlMangas = Uri.parse("$apiUrl/mangas").buildUpon() val urlMangas = Uri.parse("$apiUrl/mangas").buildUpon()
.appendPath(track.media_id.toString()) .appendPath(track.media_id.toString())
.build() .build()
val requestMangas = Request.Builder() val requestMangas = Request.Builder()
.url(urlMangas.toString()) .url(urlMangas.toString())
.get() .get()
.build() .build()
return authClient.newCall(requestMangas) return authClient.newCall(requestMangas)
.asObservableSuccess() .asObservableSuccess()
.map { netResponse -> .map { netResponse ->
val responseBody = netResponse.body?.string().orEmpty() val responseBody = netResponse.body?.string().orEmpty()
JsonParser.parseString(responseBody).obj JsonParser.parseString(responseBody).obj
}.flatMap { mangas -> }.flatMap { mangas ->
authClient.newCall(request) authClient.newCall(request)
.asObservableSuccess() .asObservableSuccess()
.map { netResponse -> .map { netResponse ->
val responseBody = netResponse.body?.string().orEmpty() val responseBody = netResponse.body?.string().orEmpty()
if (responseBody.isEmpty()) { if (responseBody.isEmpty()) {
throw Exception("Null Response") throw Exception("Null Response")
} }
val response = JsonParser.parseString(responseBody).array val response = JsonParser.parseString(responseBody).array
if (response.size() > 1) { if (response.size() > 1) {
throw Exception("Too much mangas in response") throw Exception("Too much mangas in response")
} }
val entry = response.map { val entry = response.map {
jsonToTrack(it.obj, mangas) jsonToTrack(it.obj, mangas)
} }
entry.firstOrNull() entry.firstOrNull()
} }
} }
} }
fun getCurrentUser(): Int { fun getCurrentUser(): Int {
@ -159,14 +159,15 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
} }
} }
private fun accessTokenRequest(code: String) = POST(oauthUrl, private fun accessTokenRequest(code: String) = POST(
body = FormBody.Builder() oauthUrl,
.add("grant_type", "authorization_code") body = FormBody.Builder()
.add("client_id", clientId) .add("grant_type", "authorization_code")
.add("client_secret", clientSecret) .add("client_id", clientId)
.add("code", code) .add("client_secret", clientSecret)
.add("redirect_uri", redirectUrl) .add("code", code)
.build() .add("redirect_uri", redirectUrl)
.build()
) )
companion object { companion object {
@ -186,18 +187,20 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
} }
fun authUrl() = fun authUrl() =
Uri.parse(loginUrl).buildUpon() Uri.parse(loginUrl).buildUpon()
.appendQueryParameter("client_id", clientId) .appendQueryParameter("client_id", clientId)
.appendQueryParameter("redirect_uri", redirectUrl) .appendQueryParameter("redirect_uri", redirectUrl)
.appendQueryParameter("response_type", "code") .appendQueryParameter("response_type", "code")
.build() .build()
fun refreshTokenRequest(token: String) = POST(oauthUrl, fun refreshTokenRequest(token: String) = POST(
body = FormBody.Builder() oauthUrl,
.add("grant_type", "refresh_token") body = FormBody.Builder()
.add("client_id", clientId) .add("grant_type", "refresh_token")
.add("client_secret", clientSecret) .add("client_id", clientId)
.add("refresh_token", token) .add("client_secret", clientSecret)
.build()) .add("refresh_token", token)
.build()
)
} }
} }

View File

@ -29,9 +29,9 @@ class ShikimoriInterceptor(val shikimori: Shikimori, val gson: Gson) : Intercept
} }
// Add the authorization header to the original request. // Add the authorization header to the original request.
val authRequest = originalRequest.newBuilder() val authRequest = originalRequest.newBuilder()
.addHeader("Authorization", "Bearer ${oauth!!.access_token}") .addHeader("Authorization", "Bearer ${oauth!!.access_token}")
.header("User-Agent", "Tachiyomi") .header("User-Agent", "Tachiyomi")
.build() .build()
return chain.proceed(authRequest) return chain.proceed(authRequest)
} }

View File

@ -18,7 +18,7 @@ import java.util.concurrent.TimeUnit
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
class UpdaterJob(private val context: Context, workerParams: WorkerParameters) : class UpdaterJob(private val context: Context, workerParams: WorkerParameters) :
Worker(context, workerParams) { Worker(context, workerParams) {
override fun doWork(): Result { override fun doWork(): Result {
return runBlocking { return runBlocking {
@ -37,9 +37,11 @@ class UpdaterJob(private val context: Context, workerParams: WorkerParameters) :
setContentText(context.getString(R.string.update_check_notification_update_available)) setContentText(context.getString(R.string.update_check_notification_update_available))
setSmallIcon(android.R.drawable.stat_sys_download_done) setSmallIcon(android.R.drawable.stat_sys_download_done)
// Download action // Download action
addAction(android.R.drawable.stat_sys_download_done, addAction(
context.getString(R.string.action_download), android.R.drawable.stat_sys_download_done,
PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)) context.getString(R.string.action_download),
PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
)
} }
} }
Result.success() Result.success()
@ -59,15 +61,16 @@ class UpdaterJob(private val context: Context, workerParams: WorkerParameters) :
fun setupTask(context: Context) { fun setupTask(context: Context) {
val constraints = Constraints.Builder() val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED) .setRequiredNetworkType(NetworkType.CONNECTED)
.build() .build()
val request = PeriodicWorkRequestBuilder<UpdaterJob>( val request = PeriodicWorkRequestBuilder<UpdaterJob>(
3, TimeUnit.DAYS, 3, TimeUnit.DAYS,
3, TimeUnit.HOURS) 3, TimeUnit.HOURS
.addTag(TAG) )
.setConstraints(constraints) .addTag(TAG)
.build() .setConstraints(constraints)
.build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request) WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request)
} }

View File

@ -69,13 +69,17 @@ internal class UpdaterNotifier(private val context: Context) {
setProgress(0, 0, false) setProgress(0, 0, false)
// Install action // Install action
setContentIntent(NotificationHandler.installApkPendingActivity(context, uri)) setContentIntent(NotificationHandler.installApkPendingActivity(context, uri))
addAction(R.drawable.ic_system_update_alt_white_24dp, addAction(
context.getString(R.string.action_install), R.drawable.ic_system_update_alt_white_24dp,
NotificationHandler.installApkPendingActivity(context, uri)) context.getString(R.string.action_install),
NotificationHandler.installApkPendingActivity(context, uri)
)
// Cancel action // Cancel action
addAction(R.drawable.ic_close_24dp, addAction(
context.getString(R.string.action_cancel), R.drawable.ic_close_24dp,
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER)) context.getString(R.string.action_cancel),
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER)
)
} }
notificationBuilder.show() notificationBuilder.show()
} }
@ -92,13 +96,17 @@ internal class UpdaterNotifier(private val context: Context) {
setOnlyAlertOnce(false) setOnlyAlertOnce(false)
setProgress(0, 0, false) setProgress(0, 0, false)
// Retry action // Retry action
addAction(R.drawable.ic_refresh_24dp, addAction(
context.getString(R.string.action_retry), R.drawable.ic_refresh_24dp,
UpdaterService.downloadApkPendingService(context, url)) context.getString(R.string.action_retry),
UpdaterService.downloadApkPendingService(context, url)
)
// Cancel action // Cancel action
addAction(R.drawable.ic_close_24dp, addAction(
context.getString(R.string.action_cancel), R.drawable.ic_close_24dp,
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER)) context.getString(R.string.action_cancel),
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER)
)
} }
notificationBuilder.show(Notifications.ID_UPDATER) notificationBuilder.show(Notifications.ID_UPDATER)
} }

View File

@ -16,8 +16,8 @@ class DevRepoUpdateChecker : UpdateChecker() {
private val client: OkHttpClient by lazy { private val client: OkHttpClient by lazy {
Injekt.get<NetworkHelper>().client.newBuilder() Injekt.get<NetworkHelper>().client.newBuilder()
.followRedirects(false) .followRedirects(false)
.build() .build()
} }
private val versionRegex: Regex by lazy { private val versionRegex: Regex by lazy {

View File

@ -15,10 +15,10 @@ interface GithubService {
companion object { companion object {
fun create(): GithubService { fun create(): GithubService {
val restAdapter = Retrofit.Builder() val restAdapter = Retrofit.Builder()
.baseUrl("https://api.github.com") .baseUrl("https://api.github.com")
.addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create())
.client(Injekt.get<NetworkHelper>().client) .client(Injekt.get<NetworkHelper>().client)
.build() .build()
return restAdapter.create(GithubService::class.java) return restAdapter.create(GithubService::class.java)
} }

View File

@ -119,16 +119,16 @@ class ExtensionManager(
val extensions = ExtensionLoader.loadExtensions(context) val extensions = ExtensionLoader.loadExtensions(context)
installedExtensions = extensions installedExtensions = extensions
.filterIsInstance<LoadResult.Success>() .filterIsInstance<LoadResult.Success>()
.map { it.extension } .map { it.extension }
installedExtensions installedExtensions
.flatMap { it.sources } .flatMap { it.sources }
// overwrite is needed until the bundled sources are removed // overwrite is needed until the bundled sources are removed
.forEach { sourceManager.registerSource(it, true) } .forEach { sourceManager.registerSource(it, true) }
untrustedExtensions = extensions untrustedExtensions = extensions
.filterIsInstance<LoadResult.Untrusted>() .filterIsInstance<LoadResult.Untrusted>()
.map { it.extension } .map { it.extension }
} }
/** /**
@ -223,7 +223,7 @@ class ExtensionManager(
*/ */
fun updateExtension(extension: Extension.Installed): Observable<InstallStep> { fun updateExtension(extension: Extension.Installed): Observable<InstallStep> {
val availableExt = availableExtensions.find { it.pkgName == extension.pkgName } val availableExt = availableExtensions.find { it.pkgName == extension.pkgName }
?: return Observable.empty() ?: return Observable.empty()
return installExtension(availableExt) return installExtension(availableExt)
} }
@ -266,15 +266,15 @@ class ExtensionManager(
val ctx = context val ctx = context
launchNow { launchNow {
nowTrustedExtensions nowTrustedExtensions
.map { extension -> .map { extension ->
async { ExtensionLoader.loadExtensionFromPkgName(ctx, extension.pkgName) } async { ExtensionLoader.loadExtensionFromPkgName(ctx, extension.pkgName) }
} }
.map { it.await() } .map { it.await() }
.forEach { result -> .forEach { result ->
if (result is LoadResult.Success) { if (result is LoadResult.Success) {
registerNewExtension(result.extension) registerNewExtension(result.extension)
}
} }
}
} }
} }

View File

@ -40,7 +40,8 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam
private fun createUpdateNotification(names: List<String>) { private fun createUpdateNotification(names: List<String>) {
NotificationManagerCompat.from(context).apply { NotificationManagerCompat.from(context).apply {
notify(Notifications.ID_UPDATES_TO_EXTS, notify(
Notifications.ID_UPDATES_TO_EXTS,
context.notification(Notifications.CHANNEL_UPDATES_TO_EXTS) { context.notification(Notifications.CHANNEL_UPDATES_TO_EXTS) {
setContentTitle( setContentTitle(
context.resources.getQuantityString( context.resources.getQuantityString(
@ -55,7 +56,8 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam
setSmallIcon(R.drawable.ic_extension_24dp) setSmallIcon(R.drawable.ic_extension_24dp)
setContentIntent(NotificationReceiver.openExtensionsPendingActivity(context)) setContentIntent(NotificationReceiver.openExtensionsPendingActivity(context))
setAutoCancel(true) setAutoCancel(true)
}) }
)
} }
} }
@ -72,7 +74,8 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam
val request = PeriodicWorkRequestBuilder<ExtensionUpdateJob>( val request = PeriodicWorkRequestBuilder<ExtensionUpdateJob>(
12, TimeUnit.HOURS, 12, TimeUnit.HOURS,
1, TimeUnit.HOURS) 1, TimeUnit.HOURS
)
.addTag(TAG) .addTag(TAG)
.setConstraints(constraints) .setConstraints(constraints)
.build() .build()

View File

@ -70,22 +70,22 @@ internal class ExtensionGithubApi {
val json = gson.fromJson<JsonArray>(text) val json = gson.fromJson<JsonArray>(text)
return json return json
.filter { element -> .filter { element ->
val versionName = element["version"].string val versionName = element["version"].string
val libVersion = versionName.substringBeforeLast('.').toDouble() val libVersion = versionName.substringBeforeLast('.').toDouble()
libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX
} }
.map { element -> .map { element ->
val name = element["name"].string.substringAfter("Tachiyomi: ") val name = element["name"].string.substringAfter("Tachiyomi: ")
val pkgName = element["pkg"].string val pkgName = element["pkg"].string
val apkName = element["apk"].string val apkName = element["apk"].string
val versionName = element["version"].string val versionName = element["version"].string
val versionCode = element["code"].int val versionCode = element["code"].int
val lang = element["lang"].string val lang = element["lang"].string
val icon = "$REPO_URL/icon/${apkName.replace(".apk", ".png")}" 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 { fun getApkUrl(extension: Extension.Available): String {

View File

@ -18,9 +18,9 @@ class ExtensionInstallActivity : Activity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val installIntent = Intent(Intent.ACTION_INSTALL_PACKAGE) val installIntent = Intent(Intent.ACTION_INSTALL_PACKAGE)
.setDataAndType(intent.data, intent.type) .setDataAndType(intent.data, intent.type)
.putExtra(Intent.EXTRA_RETURN_RESULT, true) .putExtra(Intent.EXTRA_RETURN_RESULT, true)
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
try { try {
startActivityForResult(installIntent, INSTALL_REQUEST_CODE) startActivityForResult(installIntent, INSTALL_REQUEST_CODE)

View File

@ -19,7 +19,7 @@ import kotlinx.coroutines.async
* @param listener The listener that should be notified of extension installation events. * @param listener The listener that should be notified of extension installation events.
*/ */
internal class ExtensionInstallReceiver(private val listener: Listener) : internal class ExtensionInstallReceiver(private val listener: Listener) :
BroadcastReceiver() { BroadcastReceiver() {
/** /**
* Registers this broadcast receiver * Registers this broadcast receiver
@ -93,7 +93,7 @@ internal class ExtensionInstallReceiver(private val listener: Listener) :
*/ */
private suspend fun getExtensionFromIntent(context: Context, intent: Intent?): LoadResult { private suspend fun getExtensionFromIntent(context: Context, intent: Intent?): LoadResult {
val pkgName = getPackageNameFromIntent(intent) 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() return GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT) { ExtensionLoader.loadExtensionFromPkgName(context, pkgName) }.await()
} }

View File

@ -65,26 +65,26 @@ internal class ExtensionInstaller(private val context: Context) {
val downloadUri = Uri.parse(url) val downloadUri = Uri.parse(url)
val request = DownloadManager.Request(downloadUri) val request = DownloadManager.Request(downloadUri)
.setTitle(extension.name) .setTitle(extension.name)
.setMimeType(APK_MIME) .setMimeType(APK_MIME)
.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, downloadUri.lastPathSegment) .setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, downloadUri.lastPathSegment)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
val id = downloadManager.enqueue(request) val id = downloadManager.enqueue(request)
activeDownloads[pkgName] = id activeDownloads[pkgName] = id
downloadsRelay.filter { it.first == id } downloadsRelay.filter { it.first == id }
.map { it.second } .map { it.second }
// Poll download status // Poll download status
.mergeWith(pollStatus(id)) .mergeWith(pollStatus(id))
// Force an error if the download takes more than 3 minutes // Force an error if the download takes more than 3 minutes
.mergeWith(Observable.timer(3, TimeUnit.MINUTES).map { InstallStep.Error }) .mergeWith(Observable.timer(3, TimeUnit.MINUTES).map { InstallStep.Error })
// Stop when the application is installed or errors // Stop when the application is installed or errors
.takeUntil { it.isCompleted() } .takeUntil { it.isCompleted() }
// Always notify on main thread // Always notify on main thread
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
// Always remove the download when unsubscribed // Always remove the download when unsubscribed
.doOnUnsubscribe { deleteDownload(pkgName) } .doOnUnsubscribe { deleteDownload(pkgName) }
} }
/** /**
@ -97,25 +97,25 @@ internal class ExtensionInstaller(private val context: Context) {
val query = DownloadManager.Query().setFilterById(id) val query = DownloadManager.Query().setFilterById(id)
return Observable.interval(0, 1, TimeUnit.SECONDS) return Observable.interval(0, 1, TimeUnit.SECONDS)
// Get the current download status // Get the current download status
.map { .map {
downloadManager.query(query).use { cursor -> downloadManager.query(query).use { cursor ->
cursor.moveToFirst() cursor.moveToFirst()
cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)) cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
}
} }
// Ignore duplicate results }
.distinctUntilChanged() // Ignore duplicate results
// Stop polling when the download fails or finishes .distinctUntilChanged()
.takeUntil { it == DownloadManager.STATUS_SUCCESSFUL || it == DownloadManager.STATUS_FAILED } // Stop polling when the download fails or finishes
// Map to our model .takeUntil { it == DownloadManager.STATUS_SUCCESSFUL || it == DownloadManager.STATUS_FAILED }
.flatMap { status -> // Map to our model
when (status) { .flatMap { status ->
DownloadManager.STATUS_PENDING -> Observable.just(InstallStep.Pending) when (status) {
DownloadManager.STATUS_RUNNING -> Observable.just(InstallStep.Downloading) DownloadManager.STATUS_PENDING -> Observable.just(InstallStep.Pending)
else -> Observable.empty() 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) { fun installApk(downloadId: Long, uri: Uri) {
val intent = Intent(context, ExtensionInstallActivity::class.java) val intent = Intent(context, ExtensionInstallActivity::class.java)
.setDataAndType(uri, APK_MIME) .setDataAndType(uri, APK_MIME)
.putExtra(EXTRA_DOWNLOAD_ID, downloadId) .putExtra(EXTRA_DOWNLOAD_ID, downloadId)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
context.startActivity(intent) context.startActivity(intent)
} }
@ -140,7 +140,7 @@ internal class ExtensionInstaller(private val context: Context) {
fun uninstallApk(pkgName: String) { fun uninstallApk(pkgName: String) {
val packageUri = Uri.parse("package:$pkgName") val packageUri = Uri.parse("package:$pkgName")
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri) val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent) context.startActivity(intent)
} }
@ -227,7 +227,7 @@ internal class ExtensionInstaller(private val context: Context) {
downloadManager.query(query).use { cursor -> downloadManager.query(query).use { cursor ->
if (cursor.moveToFirst()) { if (cursor.moveToFirst()) {
val localUri = cursor.getString( val localUri = cursor.getString(
cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI) cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)
).removePrefix(FILE_SCHEME) ).removePrefix(FILE_SCHEME)
installApk(id, File(localUri).getUriCompat(context)) installApk(id, File(localUri).getUriCompat(context))

View File

@ -35,9 +35,9 @@ internal object ExtensionLoader {
* List of the trusted signatures. * List of the trusted signatures.
*/ */
var trustedSignatures = mutableSetOf<String>() + var trustedSignatures = mutableSetOf<String>() +
Injekt.get<PreferencesHelper>().trustedSignatures().get() + Injekt.get<PreferencesHelper>().trustedSignatures().get() +
// inorichi's key // inorichi's key
"7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23" "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
/** /**
* Return a list of all the installed extensions initialized concurrently. * Return a list of all the installed extensions initialized concurrently.
@ -107,8 +107,10 @@ internal object ExtensionLoader {
// Validate lib version // Validate lib version
val libVersion = versionName.substringBeforeLast('.').toDouble() val libVersion = versionName.substringBeforeLast('.').toDouble()
if (libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) { if (libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) {
val exception = Exception("Lib version is $libVersion, while only versions " + val exception = Exception(
"$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed") "Lib version is $libVersion, while only versions " +
"$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed"
)
Timber.w(exception) Timber.w(exception)
return LoadResult.Error(exception) return LoadResult.Error(exception)
} }
@ -126,29 +128,30 @@ internal object ExtensionLoader {
val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader) val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!! val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!!
.split(";") .split(";")
.map { .map {
val sourceClass = it.trim() val sourceClass = it.trim()
if (sourceClass.startsWith(".")) if (sourceClass.startsWith(".")) {
pkgInfo.packageName + sourceClass pkgInfo.packageName + sourceClass
else } else {
sourceClass sourceClass
} }
.flatMap { }
try { .flatMap {
when (val obj = Class.forName(it, false, classLoader).newInstance()) { try {
is Source -> listOf(obj) when (val obj = Class.forName(it, false, classLoader).newInstance()) {
is SourceFactory -> obj.createSources() is Source -> listOf(obj)
else -> throw Exception("Unknown source class type! ${obj.javaClass}") 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)
} }
} catch (e: Throwable) {
Timber.e(e, "Extension load error: $extName.")
return LoadResult.Error(e)
} }
}
val langs = sources.filterIsInstance<CatalogueSource>() val langs = sources.filterIsInstance<CatalogueSource>()
.map { it.lang } .map { it.lang }
.toSet() .toSet()
val lang = when (langs.size) { val lang = when (langs.size) {
0 -> "" 0 -> ""

View File

@ -44,9 +44,9 @@ class AndroidCookieJar : CookieJar {
} }
cookies.split(";") cookies.split(";")
.map { it.substringBefore("=") } .map { it.substringBefore("=") }
.filterNames() .filterNames()
.onEach { manager.setCookie(urlString, "$it=;Max-Age=$maxAge") } .onEach { manager.setCookie(urlString, "$it=;Max-Age=$maxAge") }
} }
fun removeAll() { fun removeAll() {

View File

@ -54,7 +54,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
response.close() response.close()
networkHelper.cookieManager.remove(originalRequest.url, COOKIE_NAMES, 0) networkHelper.cookieManager.remove(originalRequest.url, COOKIE_NAMES, 0)
val oldCookie = networkHelper.cookieManager.get(originalRequest.url) val oldCookie = networkHelper.cookieManager.get(originalRequest.url)
.firstOrNull { it.name == "cf_clearance" } .firstOrNull { it.name == "cf_clearance" }
resolveWithWebView(originalRequest, oldCookie) resolveWithWebView(originalRequest, oldCookie)
return chain.proceed(originalRequest) 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 // Avoid set empty User-Agent, Chromium WebView will reset to default if empty
webview.settings.userAgentString = request.header("User-Agent") webview.settings.userAgentString = request.header("User-Agent")
?: HttpSource.DEFAULT_USERAGENT ?: HttpSource.DEFAULT_USERAGENT
webview.webViewClient = object : WebViewClientCompat() { webview.webViewClient = object : WebViewClientCompat() {
override fun onPageFinished(view: WebView, url: String) { override fun onPageFinished(view: WebView, url: String) {
fun isCloudFlareBypassed(): Boolean { fun isCloudFlareBypassed(): Boolean {
return networkHelper.cookieManager.get(origRequestUrl.toHttpUrl()) return networkHelper.cookieManager.get(origRequestUrl.toHttpUrl())
.firstOrNull { it.name == "cf_clearance" } .firstOrNull { it.name == "cf_clearance" }
.let { it != null && it != oldCookie } .let { it != null && it != oldCookie }
} }
if (isCloudFlareBypassed()) { if (isCloudFlareBypassed()) {

View File

@ -14,12 +14,12 @@ class NetworkHelper(context: Context) {
val cookieManager = AndroidCookieJar() val cookieManager = AndroidCookieJar()
val client = OkHttpClient.Builder() val client = OkHttpClient.Builder()
.cookieJar(cookieManager) .cookieJar(cookieManager)
.cache(Cache(cacheDir, cacheSize)) .cache(Cache(cacheDir, cacheSize))
.build() .build()
val cloudflareClient = client.newBuilder() val cloudflareClient = client.newBuilder()
.addInterceptor(UserAgentInterceptor()) .addInterceptor(UserAgentInterceptor())
.addInterceptor(CloudflareInterceptor(context)) .addInterceptor(CloudflareInterceptor(context))
.build() .build()
} }

View File

@ -92,14 +92,14 @@ fun Call.asObservableSuccess(): Observable<Response> {
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call { fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
val progressClient = newBuilder() val progressClient = newBuilder()
.cache(null) .cache(null)
.addNetworkInterceptor { chain -> .addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request()) val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder() originalResponse.newBuilder()
.body(ProgressResponseBody(originalResponse.body!!, listener)) .body(ProgressResponseBody(originalResponse.body!!, listener))
.build() .build()
} }
.build() .build()
return progressClient.newCall(request) return progressClient.newCall(request)
} }

View File

@ -17,10 +17,10 @@ fun GET(
cache: CacheControl = DEFAULT_CACHE_CONTROL cache: CacheControl = DEFAULT_CACHE_CONTROL
): Request { ): Request {
return Request.Builder() return Request.Builder()
.url(url) .url(url)
.headers(headers) .headers(headers)
.cacheControl(cache) .cacheControl(cache)
.build() .build()
} }
fun POST( fun POST(
@ -30,9 +30,9 @@ fun POST(
cache: CacheControl = DEFAULT_CACHE_CONTROL cache: CacheControl = DEFAULT_CACHE_CONTROL
): Request { ): Request {
return Request.Builder() return Request.Builder()
.url(url) .url(url)
.post(body) .post(body)
.headers(headers) .headers(headers)
.cacheControl(cache) .cacheControl(cache)
.build() .build()
} }

View File

@ -10,10 +10,10 @@ class UserAgentInterceptor : Interceptor {
return if (originalRequest.header("User-Agent").isNullOrEmpty()) { return if (originalRequest.header("User-Agent").isNullOrEmpty()) {
val newRequest = originalRequest val newRequest = originalRequest
.newBuilder() .newBuilder()
.removeHeader("User-Agent") .removeHeader("User-Agent")
.addHeader("User-Agent", HttpSource.DEFAULT_USERAGENT) .addHeader("User-Agent", HttpSource.DEFAULT_USERAGENT)
.build() .build()
chain.proceed(newRequest) chain.proceed(newRequest)
} else { } else {
chain.proceed(originalRequest) chain.proceed(originalRequest)

View File

@ -76,23 +76,25 @@ class LocalSource(private val context: Context) : CatalogueSource {
val time = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L val time = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
var mangaDirs = baseDirs.mapNotNull { it.listFiles()?.toList() } var mangaDirs = baseDirs.mapNotNull { it.listFiles()?.toList() }
.flatten() .flatten()
.filter { it.isDirectory && if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time } .filter { it.isDirectory && if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time }
.distinctBy { it.name } .distinctBy { it.name }
val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state
when (state?.index) { when (state?.index) {
0 -> { 0 -> {
mangaDirs = if (state.ascending) mangaDirs = if (state.ascending) {
mangaDirs.sortedBy { it.name.toLowerCase(Locale.ENGLISH) } mangaDirs.sortedBy { it.name.toLowerCase(Locale.ENGLISH) }
else } else {
mangaDirs.sortedByDescending { it.name.toLowerCase(Locale.ENGLISH) } mangaDirs.sortedByDescending { it.name.toLowerCase(Locale.ENGLISH) }
}
} }
1 -> { 1 -> {
mangaDirs = if (state.ascending) mangaDirs = if (state.ascending) {
mangaDirs.sortedBy(File::lastModified) mangaDirs.sortedBy(File::lastModified)
else } else {
mangaDirs.sortedByDescending(File::lastModified) mangaDirs.sortedByDescending(File::lastModified)
}
} }
} }
@ -131,47 +133,49 @@ class LocalSource(private val context: Context) : CatalogueSource {
override fun fetchMangaDetails(manga: SManga): Observable<SManga> { override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
getBaseDirectories(context) getBaseDirectories(context)
.mapNotNull { File(it, manga.url).listFiles()?.toList() } .mapNotNull { File(it, manga.url).listFiles()?.toList() }
.flatten() .flatten()
.firstOrNull { it.extension == "json" } .firstOrNull { it.extension == "json" }
?.apply { ?.apply {
val json = Gson().fromJson(Scanner(this).useDelimiter("\\Z").next(), JsonObject::class.java) val json = Gson().fromJson(Scanner(this).useDelimiter("\\Z").next(), JsonObject::class.java)
manga.title = json["title"]?.asString ?: manga.title manga.title = json["title"]?.asString ?: manga.title
manga.author = json["author"]?.asString ?: manga.author manga.author = json["author"]?.asString ?: manga.author
manga.artist = json["artist"]?.asString ?: manga.artist manga.artist = json["artist"]?.asString ?: manga.artist
manga.description = json["description"]?.asString ?: manga.description manga.description = json["description"]?.asString ?: manga.description
manga.genre = json["genre"]?.asJsonArray?.joinToString(", ") { it.asString } manga.genre = json["genre"]?.asJsonArray?.joinToString(", ") { it.asString }
?: manga.genre ?: manga.genre
manga.status = json["status"]?.asInt ?: manga.status manga.status = json["status"]?.asInt ?: manga.status
} }
return Observable.just(manga) return Observable.just(manga)
} }
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> { override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
val chapters = getBaseDirectories(context) val chapters = getBaseDirectories(context)
.asSequence() .asSequence()
.mapNotNull { File(it, manga.url).listFiles()?.toList() } .mapNotNull { File(it, manga.url).listFiles()?.toList() }
.flatten() .flatten()
.filter { it.isDirectory || isSupportedFile(it.extension) } .filter { it.isDirectory || isSupportedFile(it.extension) }
.map { chapterFile -> .map { chapterFile ->
SChapter.create().apply { SChapter.create().apply {
url = "${manga.url}/${chapterFile.name}" url = "${manga.url}/${chapterFile.name}"
val chapName = if (chapterFile.isDirectory) { val chapName = if (chapterFile.isDirectory) {
chapterFile.name chapterFile.name
} else { } else {
chapterFile.nameWithoutExtension 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)
} }
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) val c = c2.chapter_number.compareTo(c1.chapter_number)
if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c
}) }
.toList() )
.toList()
return Observable.just(chapters) return Observable.just(chapters)
} }
@ -215,16 +219,16 @@ class LocalSource(private val context: Context) : CatalogueSource {
return when (val format = getFormat(chapter)) { return when (val format = getFormat(chapter)) {
is Format.Directory -> { is Format.Directory -> {
val entry = format.file.listFiles() val entry = format.file.listFiles()
.sortedWith(Comparator<File> { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }) .sortedWith(Comparator<File> { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) })
.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } } .find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
entry?.let { updateCover(context, manga, it.inputStream()) } entry?.let { updateCover(context, manga, it.inputStream()) }
} }
is Format.Zip -> { is Format.Zip -> {
ZipFile(format.file).use { zip -> ZipFile(format.file).use { zip ->
val entry = zip.entries().toList() val entry = zip.entries().toList()
.sortedWith(Comparator<ZipEntry> { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }) .sortedWith(Comparator<ZipEntry> { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) })
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } } .find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
entry?.let { updateCover(context, manga, 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 -> { is Format.Rar -> {
Archive(format.file).use { archive -> Archive(format.file).use { archive ->
val entry = archive.fileHeaders val entry = archive.fileHeaders
.sortedWith(Comparator<FileHeader> { f1, f2 -> f1.fileNameString.compareToCaseInsensitiveNaturalOrder(f2.fileNameString) }) .sortedWith(Comparator<FileHeader> { f1, f2 -> f1.fileNameString.compareToCaseInsensitiveNaturalOrder(f2.fileNameString) })
.find { !it.isDirectory && ImageUtil.isImage(it.fileNameString) { archive.getInputStream(it) } } .find { !it.isDirectory && ImageUtil.isImage(it.fileNameString) { archive.getInputStream(it) } }
entry?.let { updateCover(context, manga, 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 -> { is Format.Epub -> {
EpubFile(format.file).use { epub -> EpubFile(format.file).use { epub ->
val entry = epub.getImagesFromPages() val entry = epub.getImagesFromPages()
.firstOrNull() .firstOrNull()
?.let { epub.getEntry(it) } ?.let { epub.getEntry(it) }
entry?.let { updateCover(context, manga, epub.getInputStream(it)) } entry?.let { updateCover(context, manga, epub.getInputStream(it)) }
} }

View File

@ -46,7 +46,7 @@ open class SourceManager(private val context: Context) {
} }
private fun createInternalSources(): List<Source> = listOf( private fun createInternalSources(): List<Source> = listOf(
LocalSource(context) LocalSource(context)
) )
private inner class StubSource(override val id: Long) : Source { private inner class StubSource(override val id: Long) : Source {

View File

@ -23,25 +23,31 @@ interface SManga : Serializable {
var initialized: Boolean var initialized: Boolean
fun copyFrom(other: SManga) { fun copyFrom(other: SManga) {
if (other.author != null) if (other.author != null) {
author = other.author author = other.author
}
if (other.artist != null) if (other.artist != null) {
artist = other.artist artist = other.artist
}
if (other.description != null) if (other.description != null) {
description = other.description description = other.description
}
if (other.genre != null) if (other.genre != null) {
genre = other.genre genre = other.genre
}
if (other.thumbnail_url != null) if (other.thumbnail_url != null) {
thumbnail_url = other.thumbnail_url thumbnail_url = other.thumbnail_url
}
status = other.status status = other.status
if (!initialized) if (!initialized) {
initialized = other.initialized initialized = other.initialized
}
} }
companion object { companion object {

View File

@ -90,10 +90,10 @@ abstract class HttpSource : CatalogueSource {
*/ */
override fun fetchPopularManga(page: Int): Observable<MangasPage> { override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return client.newCall(popularMangaRequest(page)) return client.newCall(popularMangaRequest(page))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map { response ->
popularMangaParse(response) popularMangaParse(response)
} }
} }
/** /**
@ -120,10 +120,10 @@ abstract class HttpSource : CatalogueSource {
*/ */
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> { override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return client.newCall(searchMangaRequest(page, query, filters)) return client.newCall(searchMangaRequest(page, query, filters))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map { response ->
searchMangaParse(response) searchMangaParse(response)
} }
} }
/** /**
@ -149,10 +149,10 @@ abstract class HttpSource : CatalogueSource {
*/ */
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> { override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
return client.newCall(latestUpdatesRequest(page)) return client.newCall(latestUpdatesRequest(page))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map { response ->
latestUpdatesParse(response) latestUpdatesParse(response)
} }
} }
/** /**
@ -177,10 +177,10 @@ abstract class HttpSource : CatalogueSource {
*/ */
override fun fetchMangaDetails(manga: SManga): Observable<SManga> { override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga)) return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map { response ->
mangaDetailsParse(response).apply { initialized = true } mangaDetailsParse(response).apply { initialized = true }
} }
} }
/** /**
@ -209,10 +209,10 @@ abstract class HttpSource : CatalogueSource {
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> { override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return if (manga.status != SManga.LICENSED) { return if (manga.status != SManga.LICENSED) {
client.newCall(chapterListRequest(manga)) client.newCall(chapterListRequest(manga))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map { response ->
chapterListParse(response) chapterListParse(response)
} }
} else { } else {
Observable.error(Exception("Licensed - No chapters to show")) Observable.error(Exception("Licensed - No chapters to show"))
} }
@ -242,10 +242,10 @@ abstract class HttpSource : CatalogueSource {
*/ */
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> { override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
return client.newCall(pageListRequest(chapter)) return client.newCall(pageListRequest(chapter))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map { response ->
pageListParse(response) pageListParse(response)
} }
} }
/** /**
@ -273,8 +273,8 @@ abstract class HttpSource : CatalogueSource {
*/ */
open fun fetchImageUrl(page: Page): Observable<String> { open fun fetchImageUrl(page: Page): Observable<String> {
return client.newCall(imageUrlRequest(page)) return client.newCall(imageUrlRequest(page))
.asObservableSuccess() .asObservableSuccess()
.map { imageUrlParse(it) } .map { imageUrlParse(it) }
} }
/** /**
@ -301,7 +301,7 @@ abstract class HttpSource : CatalogueSource {
*/ */
fun fetchImage(page: Page): Observable<Response> { fun fetchImage(page: Page): Observable<Response> {
return client.newCallWithProgress(imageRequest(page), page) return client.newCallWithProgress(imageRequest(page), page)
.asObservableSuccess() .asObservableSuccess()
} }
/** /**
@ -343,10 +343,12 @@ abstract class HttpSource : CatalogueSource {
return try { return try {
val uri = URI(orig) val uri = URI(orig)
var out = uri.path var out = uri.path
if (uri.query != null) if (uri.query != null) {
out += "?" + uri.query out += "?" + uri.query
if (uri.fragment != null) }
if (uri.fragment != null) {
out += "#" + uri.fragment out += "#" + uri.fragment
}
out out
} catch (e: URISyntaxException) { } catch (e: URISyntaxException) {
orig orig

View File

@ -6,20 +6,20 @@ import rx.Observable
fun HttpSource.getImageUrl(page: Page): Observable<Page> { fun HttpSource.getImageUrl(page: Page): Observable<Page> {
page.status = Page.LOAD_PAGE page.status = Page.LOAD_PAGE
return fetchImageUrl(page) return fetchImageUrl(page)
.doOnError { page.status = Page.ERROR } .doOnError { page.status = Page.ERROR }
.onErrorReturn { null } .onErrorReturn { null }
.doOnNext { page.imageUrl = it } .doOnNext { page.imageUrl = it }
.map { page } .map { page }
} }
fun HttpSource.fetchAllImageUrlsFromPageList(pages: List<Page>): Observable<Page> { fun HttpSource.fetchAllImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
return Observable.from(pages) return Observable.from(pages)
.filter { !it.imageUrl.isNullOrEmpty() } .filter { !it.imageUrl.isNullOrEmpty() }
.mergeWith(fetchRemainingImageUrlsFromPageList(pages)) .mergeWith(fetchRemainingImageUrlsFromPageList(pages))
} }
fun HttpSource.fetchRemainingImageUrlsFromPageList(pages: List<Page>): Observable<Page> { fun HttpSource.fetchRemainingImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
return Observable.from(pages) return Observable.from(pages)
.filter { it.imageUrl.isNullOrEmpty() } .filter { it.imageUrl.isNullOrEmpty() }
.concatMap { getImageUrl(it) } .concatMap { getImageUrl(it) }
} }

View File

@ -59,17 +59,19 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
setTheme(when (preferences.themeMode().get()) { setTheme(
Values.THEME_MODE_SYSTEM -> { when (preferences.themeMode().get()) {
if (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES) { Values.THEME_MODE_SYSTEM -> {
darkTheme if (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES) {
} else { darkTheme
lightTheme } else {
lightTheme
}
} }
Values.THEME_MODE_DARK -> darkTheme
else -> lightTheme
} }
Values.THEME_MODE_DARK -> darkTheme )
else -> lightTheme
})
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)

View File

@ -15,8 +15,9 @@ import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.clearFindViewByIdCache import kotlinx.android.synthetic.clearFindViewByIdCache
import timber.log.Timber import timber.log.Timber
abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) : RestoreViewOnCreateController(bundle), abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
LayoutContainer { RestoreViewOnCreateController(bundle),
LayoutContainer {
lateinit var binding: VB lateinit var binding: VB

View File

@ -30,6 +30,6 @@ fun Controller.requestPermissionsSafe(permissions: Array<String>, requestCode: I
fun Controller.withFadeTransaction(): RouterTransaction { fun Controller.withFadeTransaction(): RouterTransaction {
return RouterTransaction.with(this) return RouterTransaction.with(this)
.pushChangeHandler(FadeChangeHandler()) .pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler()) .popChangeHandler(FadeChangeHandler())
} }

View File

@ -87,10 +87,12 @@ abstract class DialogController : RestoreViewOnCreateController {
*/ */
fun showDialog(router: Router, tag: String?) { fun showDialog(router: Router, tag: String?) {
dismissed = false dismissed = false
router.pushController(RouterTransaction.with(this) router.pushController(
RouterTransaction.with(this)
.pushChangeHandler(SimpleSwapChangeHandler(false)) .pushChangeHandler(SimpleSwapChangeHandler(false))
.popChangeHandler(SimpleSwapChangeHandler(false)) .popChangeHandler(SimpleSwapChangeHandler(false))
.tag(tag)) .tag(tag)
)
} }
/** /**

View File

@ -44,12 +44,10 @@ abstract class RxController<VB : ViewBinding>(bundle: Bundle? = null) : BaseCont
} }
fun <T> Observable<T>.subscribeUntilDetach(): Subscription { fun <T> Observable<T>.subscribeUntilDetach(): Subscription {
return subscribe().also { untilDetachSubscriptions.add(it) } return subscribe().also { untilDetachSubscriptions.add(it) }
} }
fun <T> Observable<T>.subscribeUntilDetach(onNext: (T) -> Unit): Subscription { fun <T> Observable<T>.subscribeUntilDetach(onNext: (T) -> Unit): Subscription {
return subscribe(onNext).also { untilDetachSubscriptions.add(it) } return subscribe(onNext).also { untilDetachSubscriptions.add(it) }
} }
@ -57,7 +55,6 @@ abstract class RxController<VB : ViewBinding>(bundle: Bundle? = null) : BaseCont
onNext: (T) -> Unit, onNext: (T) -> Unit,
onError: (Throwable) -> Unit onError: (Throwable) -> Unit
): Subscription { ): Subscription {
return subscribe(onNext, onError).also { untilDetachSubscriptions.add(it) } return subscribe(onNext, onError).also { untilDetachSubscriptions.add(it) }
} }
@ -66,17 +63,14 @@ abstract class RxController<VB : ViewBinding>(bundle: Bundle? = null) : BaseCont
onError: (Throwable) -> Unit, onError: (Throwable) -> Unit,
onCompleted: () -> Unit onCompleted: () -> Unit
): Subscription { ): Subscription {
return subscribe(onNext, onError, onCompleted).also { untilDetachSubscriptions.add(it) } return subscribe(onNext, onError, onCompleted).also { untilDetachSubscriptions.add(it) }
} }
fun <T> Observable<T>.subscribeUntilDestroy(): Subscription { fun <T> Observable<T>.subscribeUntilDestroy(): Subscription {
return subscribe().also { untilDestroySubscriptions.add(it) } return subscribe().also { untilDestroySubscriptions.add(it) }
} }
fun <T> Observable<T>.subscribeUntilDestroy(onNext: (T) -> Unit): Subscription { fun <T> Observable<T>.subscribeUntilDestroy(onNext: (T) -> Unit): Subscription {
return subscribe(onNext).also { untilDestroySubscriptions.add(it) } return subscribe(onNext).also { untilDestroySubscriptions.add(it) }
} }
@ -84,7 +78,6 @@ abstract class RxController<VB : ViewBinding>(bundle: Bundle? = null) : BaseCont
onNext: (T) -> Unit, onNext: (T) -> Unit,
onError: (Throwable) -> Unit onError: (Throwable) -> Unit
): Subscription { ): Subscription {
return subscribe(onNext, onError).also { untilDestroySubscriptions.add(it) } return subscribe(onNext, onError).also { untilDestroySubscriptions.add(it) }
} }
@ -93,7 +86,6 @@ abstract class RxController<VB : ViewBinding>(bundle: Bundle? = null) : BaseCont
onError: (Throwable) -> Unit, onError: (Throwable) -> Unit,
onCompleted: () -> Unit onCompleted: () -> Unit
): Subscription { ): Subscription {
return subscribe(onNext, onError, onCompleted).also { untilDestroySubscriptions.add(it) } return subscribe(onNext, onError, onCompleted).also { untilDestroySubscriptions.add(it) }
} }
} }

View File

@ -59,11 +59,11 @@ open class BasePresenter<V> : RxPresenter<V>() {
override fun call(observable: Observable<T>): Observable<Delivery<View, T>> { override fun call(observable: Observable<T>): Observable<Delivery<View, T>> {
return observable return observable
.materialize() .materialize()
.filter { notification -> !notification.isOnCompleted } .filter { notification -> !notification.isOnCompleted }
.flatMap { notification -> .flatMap { notification ->
view.take(1).filter { it != null }.map { Delivery(it, notification) } view.take(1).filter { it != null }.map { Delivery(it, notification) }
} }
} }
} }
} }

View File

@ -8,7 +8,7 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
* @param controller The containing controller. * @param controller The containing controller.
*/ */
class CategoryAdapter(controller: CategoryController) : class CategoryAdapter(controller: CategoryController) :
FlexibleAdapter<CategoryItem>(null, controller, true) { FlexibleAdapter<CategoryItem>(null, controller, true) {
/** /**
* Listener called when an item of the list is released. * Listener called when an item of the list is released.

View File

@ -25,14 +25,15 @@ import reactivecircus.flowbinding.android.view.clicks
/** /**
* Controller to manage the categories for the users' library. * Controller to manage the categories for the users' library.
*/ */
class CategoryController : NucleusController<CategoriesControllerBinding, CategoryPresenter>(), class CategoryController :
ActionMode.Callback, NucleusController<CategoriesControllerBinding, CategoryPresenter>(),
FlexibleAdapter.OnItemClickListener, ActionMode.Callback,
FlexibleAdapter.OnItemLongClickListener, FlexibleAdapter.OnItemClickListener,
CategoryAdapter.OnItemReleaseListener, FlexibleAdapter.OnItemLongClickListener,
CategoryCreateDialog.Listener, CategoryAdapter.OnItemReleaseListener,
CategoryRenameDialog.Listener, CategoryCreateDialog.Listener,
UndoHelper.OnActionListener { CategoryRenameDialog.Listener,
UndoHelper.OnActionListener {
/** /**
* Object used to show ActionMode toolbar. * Object used to show ActionMode toolbar.
@ -176,8 +177,10 @@ class CategoryController : NucleusController<CategoriesControllerBinding, Catego
when (item.itemId) { when (item.itemId) {
R.id.action_delete -> { R.id.action_delete -> {
undoHelper = UndoHelper(adapter, this) undoHelper = UndoHelper(adapter, this)
undoHelper?.start(adapter.selectedPositions, view!!, undoHelper?.start(
R.string.snack_categories_deleted, R.string.action_undo, 3000) adapter.selectedPositions, view!!,
R.string.snack_categories_deleted, R.string.action_undo, 3000
)
mode.finish() mode.finish()
} }

View File

@ -31,17 +31,17 @@ class CategoryCreateDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
*/ */
override fun onCreateDialog(savedViewState: Bundle?): Dialog { override fun onCreateDialog(savedViewState: Bundle?): Dialog {
return MaterialDialog(activity!!) return MaterialDialog(activity!!)
.title(R.string.action_add_category) .title(R.string.action_add_category)
.negativeButton(android.R.string.cancel) .negativeButton(android.R.string.cancel)
.input( .input(
hint = resources?.getString(R.string.name), hint = resources?.getString(R.string.name),
prefill = currentName prefill = currentName
) { _, input -> ) { _, input ->
currentName = input.toString() currentName = input.toString()
} }
.positiveButton(android.R.string.ok) { .positiveButton(android.R.string.ok) {
(targetController as? Listener)?.createCategory(currentName) (targetController as? Listener)?.createCategory(currentName)
} }
} }
interface Listener { interface Listener {

Some files were not shown because too many files have changed in this diff Show More