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