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