diff --git a/.travis.yml b/.travis.yml index 863820399..60217ab47 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,13 +5,16 @@ android: - tools # The BuildTools version used by your project - - build-tools-23.0.3 - - android-23 + - build-tools-24.0.2 + - android-24 - extra-android-m2repository - extra-google-m2repository - extra-android-support - extra-google-google_play_services +jdk: + - oraclejdk8 + before_script: - chmod +x gradlew #Build, and run tests diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 000000000..e69de29bb diff --git a/app/.gitignore b/app/.gitignore index 8bd2d8bd6..012bccc6a 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1,4 +1,5 @@ /build *iml *.iml -.idea \ No newline at end of file +custom.gradle +google-services.json \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 0a0e8872b..0a9d24523 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,6 +4,10 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' +if (file("custom.gradle").exists()) { + apply from: "custom.gradle" +} + ext { // Git is needed in your system PATH for these commands to work. // If it's not installed, you can return a random value as a workaround @@ -29,14 +33,14 @@ def includeUpdater() { } android { - compileSdkVersion 23 - buildToolsVersion "23.0.3" + compileSdkVersion 24 + buildToolsVersion "24.0.2" publishNonDefault true defaultConfig { applicationId "eu.kanade.tachiyomi.eh" minSdkVersion 16 - targetSdkVersion 23 + targetSdkVersion 24 testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" versionCode 2180 versionName "v2.18.0-EH" @@ -47,17 +51,22 @@ android { buildConfigField "boolean", "INCLUDE_UPDATER", "${includeUpdater()}" vectorDrawables.useSupportLibrary = true + + ndk { + abiFilters "armeabi", "armeabi-v7a", "x86" + } } buildTypes { debug { - minifyEnabled false - shrinkResources true - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + versionNameSuffix "-${getCommitCount()}" + applicationIdSuffix ".debug" + multiDexEnabled true } release { - minifyEnabled false + minifyEnabled true shrinkResources true + multiDexEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } @@ -84,11 +93,11 @@ android { dependencies { // Modified dependencies - compile 'com.github.inorichi:subsampling-scale-image-view:421fb81' + compile 'com.github.inorichi:subsampling-scale-image-view:2d9c854' compile 'com.github.inorichi:ReactiveNetwork:69092ed' // Android support library - final support_library_version = '23.4.0' + final support_library_version = '24.2.1' compile "com.android.support:support-v4:$support_library_version" compile "com.android.support:appcompat-v7:$support_library_version" compile "com.android.support:cardview-v7:$support_library_version" @@ -97,13 +106,17 @@ dependencies { compile "com.android.support:support-annotations:$support_library_version" compile "com.android.support:customtabs:$support_library_version" + compile 'com.android.support:multidex:1.0.1' + + compile 'com.google.android.gms:play-services-gcm:9.6.1' + // ReactiveX compile 'io.reactivex:rxandroid:1.2.1' - compile 'io.reactivex:rxjava:1.1.6' + compile 'io.reactivex:rxjava:1.2.1' compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.2' // Network client - compile "com.squareup.okhttp3:okhttp:3.3.1" + compile "com.squareup.okhttp3:okhttp:3.4.1" // REST final retrofit_version = '2.1.0' @@ -112,17 +125,17 @@ dependencies { compile "com.squareup.retrofit2:adapter-rxjava:$retrofit_version" // IO - compile 'com.squareup.okio:okio:1.8.0' + compile 'com.squareup.okio:okio:1.10.0' // JSON compile 'com.google.code.gson:gson:2.7' - compile 'com.github.salomonbrys.kotson:kotson:2.3.0' + compile 'com.github.salomonbrys.kotson:kotson:2.4.0' // YAML compile 'com.github.bmoliveira:snake-yaml:v1.18-android' // JavaScript engine - compile 'com.squareup.duktape:duktape-android:0.9.5' + compile 'com.squareup.duktape:duktape-android:1.0.0' // Disk cache compile 'com.jakewharton:disklrucache:2.0.2' @@ -134,7 +147,7 @@ dependencies { compile 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0' // Database - compile "com.pushtorefresh.storio:sqlite:1.9.0" + compile "com.pushtorefresh.storio:sqlite:1.11.0" // Model View Presenter final nucleus_version = '3.0.0' @@ -148,23 +161,25 @@ dependencies { // Image library compile 'com.github.bumptech.glide:glide:3.7.0' compile 'com.github.bumptech.glide:okhttp3-integration:1.4.0@aar' + // Transformations + compile 'jp.wasabeef:glide-transformations:2.0.1' // Logging - compile 'com.jakewharton.timber:timber:4.1.2' + compile 'com.jakewharton.timber:timber:4.3.1' // Crash reports - compile 'ch.acra:acra:4.9.0' + compile 'ch.acra:acra:4.9.1' // UI - compile 'com.dmitrymalkovich.android:material-design-dimens:1.2' + compile 'com.dmitrymalkovich.android:material-design-dimens:1.4' compile 'com.github.dmytrodanylyk.android-process-button:library:1.0.4' compile 'eu.davidea:flexible-adapter:4.2.0' compile 'com.nononsenseapps:filepicker:2.5.2' compile 'com.github.amulyakhare:TextDrawable:558677e' - compile 'com.afollestad.material-dialogs:core:0.8.6.1' - compile 'net.xpece.android:support-preference:0.8.1' + compile 'com.afollestad.material-dialogs:core:0.9.0.2' + compile 'net.xpece.android:support-preference:1.0.3' compile 'me.zhanghai.android.systemuihelper:library:1.0.0' - compile 'org.adw.library:discrete-seekbar:1.0.1' + compile 'de.hdodenhof:circleimageview:2.1.0' //EXH compile 'com.jakewharton:process-phoenix:1.0.2' @@ -173,13 +188,15 @@ dependencies { testCompile 'junit:junit:4.12' testCompile 'org.assertj:assertj-core:1.7.1' testCompile 'org.mockito:mockito-core:1.10.19' - testCompile 'org.robolectric:robolectric:3.1' + testCompile 'org.robolectric:robolectric:3.1.2' + testCompile 'org.robolectric:shadows-multidex:3.1.2' + testCompile 'org.robolectric:shadows-play-services:3.1.2' compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" } buildscript { - ext.kotlin_version = '1.0.3' + ext.kotlin_version = '1.0.4' repositories { mavenCentral() } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 78c41e135..74d99a928 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,5 +1,7 @@ -dontobfuscate +-keep class eu.kanade.tachiyomi.** + # OkHttp -keepattributes Signature -keepattributes *Annotation* diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b192b0128..145bcb8d3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,8 @@ - + @@ -8,6 +10,8 @@ + + + android:name=".ui.main.MainActivity"> @@ -28,7 +31,8 @@ + android:parentActivityName=".ui.main.MainActivity" + android:exported="true"> - + - + - + - + - + - + + + + + - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt index 8103fb5f1..cd2508196 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/App.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt @@ -1,6 +1,8 @@ package eu.kanade.tachiyomi import android.app.Application +import android.content.Context +import android.support.multidex.MultiDex import org.acra.ACRA import org.acra.annotation.ReportsCrashes import timber.log.Timber @@ -27,6 +29,13 @@ open class App : Application() { setupAcra() } + override fun attachBaseContext(base: Context) { + super.attachBaseContext(base) + if (BuildConfig.DEBUG) { + MultiDex.install(this) + } + } + protected open fun setupAcra() { ACRA.init(this) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaSyncQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaSyncQueries.kt deleted file mode 100644 index 5140b516e..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaSyncQueries.kt +++ /dev/null @@ -1,46 +0,0 @@ -package eu.kanade.tachiyomi.data.database.queries - -import com.pushtorefresh.storio.sqlite.queries.DeleteQuery -import com.pushtorefresh.storio.sqlite.queries.Query -import eu.kanade.tachiyomi.data.database.DbProvider -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.MangaSync -import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable -import eu.kanade.tachiyomi.data.mangasync.MangaSyncService - -interface MangaSyncQueries : DbProvider { - - fun getMangaSync(manga: Manga, sync: MangaSyncService) = db.get() - .`object`(MangaSync::class.java) - .withQuery(Query.builder() - .table(MangaSyncTable.TABLE) - .where("${MangaSyncTable.COL_MANGA_ID} = ? AND " + - "${MangaSyncTable.COL_SYNC_ID} = ?") - .whereArgs(manga.id, sync.id) - .build()) - .prepare() - - fun getMangasSync(manga: Manga) = db.get() - .listOfObjects(MangaSync::class.java) - .withQuery(Query.builder() - .table(MangaSyncTable.TABLE) - .where("${MangaSyncTable.COL_MANGA_ID} = ?") - .whereArgs(manga.id) - .build()) - .prepare() - - fun insertMangaSync(manga: MangaSync) = db.put().`object`(manga).prepare() - - fun insertMangasSync(mangas: List) = db.put().objects(mangas).prepare() - - fun deleteMangaSync(manga: MangaSync) = db.delete().`object`(manga).prepare() - - fun deleteMangaSyncForManga(manga: Manga) = db.delete() - .byQuery(DeleteQuery.builder() - .table(MangaSyncTable.TABLE) - .where("${MangaSyncTable.COL_MANGA_ID} = ?") - .whereArgs(manga.id) - .build()) - .prepare() - -} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt index 483f06ba6..1661d6a44 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt @@ -80,10 +80,10 @@ class DownloadManager( if (areAllDownloadsFinished()) { DownloadService.stop(context) } - }, { e -> + }, { error -> DownloadService.stop(context) - Timber.e(e, e.message) - downloadNotifier.onError(e.message) + Timber.e(error) + downloadNotifier.onError(error.message) }) if (!isRunning) { @@ -369,8 +369,8 @@ class DownloadManager( try { it.write(gson.toJson(pages).toByteArray()) it.flush() - } catch (e: Exception) { - Timber.e(e, e.message) + } catch (error: Exception) { + Timber.e(error) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadNotifier.kt index 01f3a7b7f..498a08883 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadNotifier.kt @@ -96,6 +96,10 @@ class DownloadNotifier(private val context: Context) { if (multipleDownloadThreads) { setContentTitle(context.getString(R.string.app_name)) + // Reset the queue size if the download progress is negative + if ((initialQueueSize - queue.size) < 0) + initialQueueSize = queue.size + setContentText(context.getString(R.string.chapter_downloading_progress) .format(initialQueueSize - queue.size, initialQueueSize)) setProgress(initialQueueSize, initialQueueSize - queue.size, false) @@ -161,6 +165,9 @@ class DownloadNotifier(private val context: Context) { setProgress(0, 0, false) } context.notificationManager.notify(Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID, notificationBuilder.build()) + + // Reset download information + onClear() isDownloading = false } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateAlarm.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateAlarm.kt deleted file mode 100644 index 1c08f967e..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateAlarm.kt +++ /dev/null @@ -1,83 +0,0 @@ -package eu.kanade.tachiyomi.data.library - -import android.app.AlarmManager -import android.app.PendingIntent -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.os.SystemClock -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.data.preference.getOrDefault -import eu.kanade.tachiyomi.util.alarmManager -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get - -/** - * This class is used to update the library by firing an alarm after a specified time. - * It has a receiver reacting to system's boot and the intent fired by this alarm. - * See [onReceive] for more information. - */ -class LibraryUpdateAlarm : BroadcastReceiver() { - - companion object { - const val LIBRARY_UPDATE_ACTION = "eu.kanade.UPDATE_LIBRARY" - - /** - * Sets the alarm to run the intent that updates the library. - * @param context the application context. - * @param intervalInHours the time in hours when it will be executed. Defaults to the - * value stored in preferences. - */ - fun startAlarm(context: Context, - intervalInHours: Int = Injekt.get().libraryUpdateInterval().getOrDefault()) { - // Stop previous running alarms if needed, and do not restart it if the interval is 0. - stopAlarm(context) - if (intervalInHours == 0) - return - - // Get the time the alarm should fire the event to update. - val intervalInMillis = intervalInHours * 60 * 60 * 1000 - val nextRun = SystemClock.elapsedRealtime() + intervalInMillis - - // Start the alarm. - val pendingIntent = getPendingIntent(context) - context.alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, - nextRun, intervalInMillis.toLong(), pendingIntent) - } - - /** - * Stops the alarm if it's running. - * @param context the application context. - */ - fun stopAlarm(context: Context) { - val pendingIntent = getPendingIntent(context) - context.alarmManager.cancel(pendingIntent) - } - - /** - * Get the intent the alarm should run when it's fired. - * @param context the application context. - * @return the intent that will run when the alarm is fired. - */ - private fun getPendingIntent(context: Context): PendingIntent { - val intent = Intent(context, LibraryUpdateAlarm::class.java) - intent.action = LIBRARY_UPDATE_ACTION - return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) - } - } - - /** - * Handle the intents received by this [BroadcastReceiver]. - * @param context the application context. - * @param intent the intent to process. - */ - override fun onReceive(context: Context, intent: Intent) { - when (intent.action) { - // Start the alarm when the system is booted. - Intent.ACTION_BOOT_COMPLETED -> startAlarm(context) - // Update the library when the alarm fires an event. - LIBRARY_UPDATE_ACTION -> LibraryUpdateService.start(context) - } - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt index b36acef0e..1b32ffca6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt @@ -5,11 +5,11 @@ import android.app.Service import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.graphics.Bitmap +import android.graphics.BitmapFactory import android.os.IBinder import android.os.PowerManager import android.support.v4.app.NotificationCompat -import com.github.pwittchen.reactivenetwork.library.ConnectivityStatus -import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork import eu.kanade.tachiyomi.Constants import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.DatabaseHelper @@ -17,10 +17,14 @@ import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.source.SourceManager import eu.kanade.tachiyomi.data.source.online.OnlineSource import eu.kanade.tachiyomi.ui.main.MainActivity -import eu.kanade.tachiyomi.util.* +import eu.kanade.tachiyomi.util.AndroidComponentUtil +import eu.kanade.tachiyomi.util.notification +import eu.kanade.tachiyomi.util.notificationManager +import eu.kanade.tachiyomi.util.syncChaptersWithSource import rx.Observable import rx.Subscription import rx.schedulers.Schedulers @@ -69,18 +73,20 @@ class LibraryUpdateService : Service() { private val notificationId: Int get() = Constants.NOTIFICATION_LIBRARY_ID + private var notificationBitmap: Bitmap? = null companion object { - /** - * Key for manual library update. - */ - const val UPDATE_IS_MANUAL = "is_manual" /** * Key for category to update. */ const val UPDATE_CATEGORY = "category" + /** + * Key for updating the details instead of the chapters. + */ + const val UPDATE_DETAILS = "details" + /** * Returns the status of the service. * @@ -96,13 +102,13 @@ class LibraryUpdateService : Service() { * running. * * @param context the application context. - * @param isManual whether the update has been manually triggered. - * @param category a specific category to update, or null for all in the library. + * @param category a specific category to update, or null for global update. + * @param details whether to update the details instead of the list of chapters. */ - fun start(context: Context, isManual: Boolean = false, category: Category? = null) { + fun start(context: Context, category: Category? = null, details: Boolean = false) { if (!isRunning(context)) { val intent = Intent(context, LibraryUpdateService::class.java).apply { - putExtra(UPDATE_IS_MANUAL, isManual) + putExtra(UPDATE_DETAILS, details) category?.let { putExtra(UPDATE_CATEGORY, it.id) } } context.startService(intent) @@ -135,7 +141,8 @@ class LibraryUpdateService : Service() { */ override fun onDestroy() { subscription?.unsubscribe() - LibraryUpdateAlarm.startAlarm(this) + notificationBitmap?.recycle() + notificationBitmap = null destroyWakeLock() super.onDestroy() } @@ -156,61 +163,36 @@ class LibraryUpdateService : Service() { * @return the start value of the command. */ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - - // Get connectivity status - val connection = ReactiveNetwork().getConnectivityStatus(this, true) - - // Get library update restrictions - val restrictions = preferences.libraryUpdateRestriction() - - // Check if users updates library manual - val isManualUpdate = intent?.getBooleanExtra(UPDATE_IS_MANUAL, false) ?: false - - // Whether to cancel the update. - var cancelUpdate = false - - // Check if device has internet connection - // Check if device has wifi connection if only wifi is enabled - if (connection == ConnectivityStatus.OFFLINE || (!isManualUpdate && "wifi" in restrictions - && connection != ConnectivityStatus.WIFI_CONNECTED_HAS_INTERNET)) { - - if (isManualUpdate) { - toast(R.string.notification_no_connection_title) - } - - // Enable library update when connection available - AndroidComponentUtil.toggleComponent(this, SyncOnConnectionAvailable::class.java, true) - cancelUpdate = true - } - if (!isManualUpdate && "ac" in restrictions && !DeviceUtil.isPowerConnected(this)) { - AndroidComponentUtil.toggleComponent(this, SyncOnPowerConnected::class.java, true) - cancelUpdate = true - } - - if (cancelUpdate) { - stopSelf(startId) - return Service.START_NOT_STICKY - } - - // Stop enabled components. - AndroidComponentUtil.toggleComponent(this, SyncOnConnectionAvailable::class.java, false) - AndroidComponentUtil.toggleComponent(this, SyncOnPowerConnected::class.java, false) + if (intent == null) return Service.START_NOT_STICKY // Unsubscribe from any previous subscription if needed. subscription?.unsubscribe() // Update favorite manga. Destroy service when completed or in case of an error. - subscription = Observable.defer { updateMangaList(getMangaToUpdate(intent)) } + subscription = Observable + .defer { + if (notificationBitmap == null) { + notificationBitmap = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher) + } + + val mangaList = getMangaToUpdate(intent) + + // Update either chapter list or manga details. + if (!intent.getBooleanExtra(UPDATE_DETAILS, false)) + updateChapterList(mangaList) + else + updateDetails(mangaList) + } .subscribeOn(Schedulers.io()) - .subscribe({}, - { - showNotification(getString(R.string.notification_update_error), "") - stopSelf(startId) - }, { + .subscribe({ + }, { + showNotification(getString(R.string.notification_update_error), "") + stopSelf(startId) + }, { stopSelf(startId) }) - return Service.START_STICKY + return Service.START_REDELIVER_INTENT } /** @@ -219,19 +201,26 @@ class LibraryUpdateService : Service() { * @param intent the update intent. * @return a list of manga to update */ - fun getMangaToUpdate(intent: Intent?): List { - val categoryId = intent?.getIntExtra(UPDATE_CATEGORY, -1) ?: -1 + fun getMangaToUpdate(intent: Intent): List { + val categoryId = intent.getIntExtra(UPDATE_CATEGORY, -1) - var toUpdate = if (categoryId != -1) + var listToUpdate = if (categoryId != -1) db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId } - else - db.getFavoriteMangas().executeAsBlocking() - - if (preferences.updateOnlyNonCompleted()) { - toUpdate = toUpdate.filter { it.status != Manga.COMPLETED } + else { + val categoriesToUpdate = preferences.libraryUpdateCategories().getOrDefault().map { it.toInt() } + if (categoriesToUpdate.isNotEmpty()) + db.getLibraryMangas().executeAsBlocking() + .filter { it.category in categoriesToUpdate } + .distinctBy { it.id } + else + db.getFavoriteMangas().executeAsBlocking().distinctBy { it.id } } - return toUpdate + if (!intent.getBooleanExtra(UPDATE_DETAILS, false) && preferences.updateOnlyNonCompleted()) { + listToUpdate = listToUpdate.filter { it.status != Manga.COMPLETED } + } + + return listToUpdate } /** @@ -243,7 +232,7 @@ class LibraryUpdateService : Service() { * @param mangaToUpdate the list to update * @return an observable delivering the progress of each update. */ - fun updateMangaList(mangaToUpdate: List): Observable { + fun updateChapterList(mangaToUpdate: List): Observable { // Initialize the variables holding the progress of the updates. val count = AtomicInteger(0) val newUpdates = ArrayList() @@ -278,6 +267,7 @@ class LibraryUpdateService : Service() { } else { showResultNotification(newUpdates, failedUpdates) } + LibraryUpdateTrigger.setupTask(this) } } @@ -293,6 +283,43 @@ class LibraryUpdateService : Service() { .map { syncChaptersWithSource(db, it, manga, source) } } + /** + * Method that updates the details of the given list of manga. It's called in a background + * thread, so it's safe to do heavy operations or network calls here. + * For each manga it calls [updateManga] and updates the notification showing the current + * progress. + * + * @param mangaToUpdate the list to update + * @return an observable delivering the progress of each update. + */ + fun updateDetails(mangaToUpdate: List): Observable { + // Initialize the variables holding the progress of the updates. + val count = AtomicInteger(0) + + val cancelIntent = PendingIntent.getBroadcast(this, 0, + Intent(this, CancelUpdateReceiver::class.java), 0) + + // Emit each manga and update it sequentially. + return Observable.from(mangaToUpdate) + // Notify manga that will update. + .doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size, cancelIntent) } + // Update the details of the manga. + .concatMap { manga -> + val source = sourceManager.get(manga.source) as? OnlineSource + ?: return@concatMap Observable.empty() + + source.fetchMangaDetails(manga) + .doOnNext { networkManga -> + manga.copyFrom(networkManga) + db.insertManga(manga).executeAsBlocking() + } + .onErrorReturn { manga } + } + .doOnCompleted { + cancelNotification() + } + } + /** * Returns the text that will be displayed in the notification when there are new chapters. * @@ -351,6 +378,7 @@ class LibraryUpdateService : Service() { private fun showNotification(title: String, body: String) { notificationManager.notify(notificationId, notification() { setSmallIcon(R.drawable.ic_refresh_white_24dp_img) + setLargeIcon(notificationBitmap) setContentTitle(title) setContentText(body) }) @@ -366,6 +394,7 @@ class LibraryUpdateService : Service() { private fun showProgressNotification(manga: Manga, current: Int, total: Int, cancelIntent: PendingIntent) { notificationManager.notify(notificationId, notification() { setSmallIcon(R.drawable.ic_refresh_white_24dp_img) + setLargeIcon(notificationBitmap) setContentTitle(manga.title) setProgress(total, current, false) setOngoing(true) @@ -386,6 +415,7 @@ class LibraryUpdateService : Service() { notificationManager.notify(notificationId, notification() { setSmallIcon(R.drawable.ic_refresh_white_24dp_img) + setLargeIcon(notificationBitmap) setContentTitle(title) setStyle(NotificationCompat.BigTextStyle().bigText(body)) setContentIntent(notificationIntent) @@ -410,41 +440,6 @@ class LibraryUpdateService : Service() { return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) } - /** - * Class that triggers the library to update when a connection is available. It receives - * network changes. - */ - class SyncOnConnectionAvailable : BroadcastReceiver() { - /** - * Method called when a network change occurs. - * - * @param context the application context. - * @param intent the intent received. - */ - override fun onReceive(context: Context, intent: Intent) { - if (DeviceUtil.isNetworkConnected(context)) { - AndroidComponentUtil.toggleComponent(context, this.javaClass, false) - start(context) - } - } - } - - /** - * Class that triggers the library to update when connected to power. - */ - class SyncOnPowerConnected: BroadcastReceiver() { - /** - * Method called when AC is connected. - * - * @param context the application context. - * @param intent the intent received. - */ - override fun onReceive(context: Context, intent: Intent) { - AndroidComponentUtil.toggleComponent(context, this.javaClass, false) - start(context) - } - } - /** * Class that stops updating the library. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateTrigger.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateTrigger.kt new file mode 100644 index 000000000..8393243cc --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateTrigger.kt @@ -0,0 +1,52 @@ +package eu.kanade.tachiyomi.data.library + +import android.content.Context +import com.google.android.gms.gcm.* +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.preference.getOrDefault +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class LibraryUpdateTrigger : GcmTaskService() { + + override fun onInitializeTasks() { + setupTask(this) + } + + override fun onRunTask(params: TaskParams): Int { + LibraryUpdateService.start(this) + return GcmNetworkManager.RESULT_SUCCESS + } + + companion object { + fun setupTask(context: Context, prefInterval: Int? = null) { + val preferences = Injekt.get() + val interval = prefInterval ?: preferences.libraryUpdateInterval().getOrDefault() + if (interval > 0) { + val restrictions = preferences.libraryUpdateRestriction() + val acRestriction = "ac" in restrictions + val wifiRestriction = if ("wifi" in restrictions) + Task.NETWORK_STATE_UNMETERED + else + Task.NETWORK_STATE_ANY + + val task = PeriodicTask.Builder() + .setService(LibraryUpdateTrigger::class.java) + .setTag("Library periodic update") + .setPeriod(interval * 60 * 60L) + .setFlex(5 * 60) + .setRequiredNetwork(wifiRestriction) + .setRequiresCharging(acRestriction) + .setUpdateCurrent(true) + .setPersisted(true) + .build() + + GcmNetworkManager.getInstance(context).schedule(task) + } + } + + fun cancelTask(context: Context) { + GcmNetworkManager.getInstance(context).cancelAllTasks(LibraryUpdateTrigger::class.java) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/MangaSyncManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/MangaSyncManager.kt deleted file mode 100644 index 1302d6221..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/MangaSyncManager.kt +++ /dev/null @@ -1,18 +0,0 @@ -package eu.kanade.tachiyomi.data.mangasync - -import android.content.Context -//import eu.kanade.tachiyomi.data.mangasync.myanimelist.MyAnimeList - -class MangaSyncManager(private val context: Context) { - - companion object { -// const val MYANIMELIST = 1 - } - -// val myAnimeList = MyAnimeList(context, MYANIMELIST) - - val services = emptyList() - - fun getService(id: Int) = services.find { it.id == id } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/MangaSyncService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/MangaSyncService.kt deleted file mode 100644 index ae653b0f0..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/MangaSyncService.kt +++ /dev/null @@ -1,51 +0,0 @@ -package eu.kanade.tachiyomi.data.mangasync - -import android.content.Context -import android.support.annotation.CallSuper -import eu.kanade.tachiyomi.data.database.models.MangaSync -import eu.kanade.tachiyomi.data.network.NetworkHelper -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import okhttp3.OkHttpClient -import rx.Completable -import rx.Observable -import uy.kohesive.injekt.injectLazy - -abstract class MangaSyncService(private val context: Context, val id: Int) { - - val preferences: PreferencesHelper by injectLazy() - val networkService: NetworkHelper by injectLazy() - - open val client: OkHttpClient - get() = networkService.client - - // Name of the manga sync service to display - abstract val name: String - - abstract fun login(username: String, password: String): Completable - - open val isLogged: Boolean - get() = !getUsername().isEmpty() && - !getPassword().isEmpty() - - abstract fun add(manga: MangaSync): Observable - - abstract fun update(manga: MangaSync): Observable - - abstract fun bind(manga: MangaSync): Observable - - abstract fun getStatus(status: Int): String - - fun saveCredentials(username: String, password: String) { - preferences.setMangaSyncCredentials(this, username, password) - } - - @CallSuper - open fun logout() { - preferences.setMangaSyncCredentials(this, "", "") - } - - fun getUsername() = preferences.mangaSyncUsername(this) - - fun getPassword() = preferences.mangaSyncPassword(this) - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/UpdateMangaSyncService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/UpdateMangaSyncService.kt deleted file mode 100644 index 84181b555..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/UpdateMangaSyncService.kt +++ /dev/null @@ -1,74 +0,0 @@ -package eu.kanade.tachiyomi.data.mangasync - -import android.app.Service -import android.content.Context -import android.content.Intent -import android.os.IBinder -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.MangaSync -import rx.Observable -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers -import rx.subscriptions.CompositeSubscription -import uy.kohesive.injekt.injectLazy - -class UpdateMangaSyncService : Service() { - - val syncManager: MangaSyncManager by injectLazy() - val db: DatabaseHelper by injectLazy() - - private lateinit var subscriptions: CompositeSubscription - - override fun onCreate() { - super.onCreate() - subscriptions = CompositeSubscription() - } - - override fun onDestroy() { - subscriptions.unsubscribe() - super.onDestroy() - } - - override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { - val manga = intent.getSerializableExtra(EXTRA_MANGASYNC) - if (manga != null) { - updateLastChapterRead(manga as MangaSync, startId) - return Service.START_REDELIVER_INTENT - } else { - stopSelf(startId) - return Service.START_NOT_STICKY - } - } - - override fun onBind(intent: Intent): IBinder? { - return null - } - - private fun updateLastChapterRead(mangaSync: MangaSync, startId: Int) { - val sync = syncManager.getService(mangaSync.sync_id) - if (sync == null) { - stopSelf(startId) - return - } - - subscriptions.add(Observable.defer { sync.update(mangaSync) } - .flatMap { db.insertMangaSync(mangaSync).asRxObservable() } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ stopSelf(startId) }, - { stopSelf(startId) })) - } - - companion object { - - private val EXTRA_MANGASYNC = "extra_mangasync" - - @JvmStatic - fun start(context: Context, mangaSync: MangaSync) { - val intent = Intent(context, UpdateMangaSyncService::class.java) - intent.putExtra(EXTRA_MANGASYNC, mangaSync) - context.startService(intent) - } - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/myanimelist/MyAnimeList.kt b/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/myanimelist/MyAnimeList.kt deleted file mode 100644 index 3b589972e..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/myanimelist/MyAnimeList.kt +++ /dev/null @@ -1,222 +0,0 @@ -package eu.kanade.tachiyomi.data.mangasync.myanimelist - -import android.content.Context -import android.net.Uri -import android.util.Xml -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.MangaSync -import eu.kanade.tachiyomi.data.mangasync.MangaSyncService -import eu.kanade.tachiyomi.data.network.GET -import eu.kanade.tachiyomi.data.network.POST -import eu.kanade.tachiyomi.data.network.asObservable -import eu.kanade.tachiyomi.util.selectInt -import eu.kanade.tachiyomi.util.selectText -import okhttp3.Credentials -import okhttp3.FormBody -import okhttp3.Headers -import okhttp3.RequestBody -import org.jsoup.Jsoup -import org.xmlpull.v1.XmlSerializer -import rx.Completable -import rx.Observable -import java.io.StringWriter - -class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(context, id) { - - private lateinit var headers: Headers - - companion object { - val BASE_URL = "http://myanimelist.net" - - private val ENTRY_TAG = "entry" - private val CHAPTER_TAG = "chapter" - private val SCORE_TAG = "score" - private val STATUS_TAG = "status" - - val READING = 1 - val COMPLETED = 2 - val ON_HOLD = 3 - val DROPPED = 4 - val PLAN_TO_READ = 6 - - val DEFAULT_STATUS = READING - val DEFAULT_SCORE = 0 - } - - init { - val username = getUsername() - val password = getPassword() - - if (!username.isEmpty() && !password.isEmpty()) { - createHeaders(username, password) - } - } - - override val name: String - get() = "MyAnimeList" - - fun getLoginUrl() = Uri.parse(BASE_URL).buildUpon() - .appendEncodedPath("api/account/verify_credentials.xml") - .toString() - - fun getSearchUrl(query: String) = Uri.parse(BASE_URL).buildUpon() - .appendEncodedPath("api/manga/search.xml") - .appendQueryParameter("q", query) - .toString() - - fun getListUrl(username: String) = Uri.parse(BASE_URL).buildUpon() - .appendPath("malappinfo.php") - .appendQueryParameter("u", username) - .appendQueryParameter("status", "all") - .appendQueryParameter("type", "manga") - .toString() - - fun getUpdateUrl(manga: MangaSync) = Uri.parse(BASE_URL).buildUpon() - .appendEncodedPath("api/mangalist/update") - .appendPath("${manga.remote_id}.xml") - .toString() - - fun getAddUrl(manga: MangaSync) = Uri.parse(BASE_URL).buildUpon() - .appendEncodedPath("api/mangalist/add") - .appendPath("${manga.remote_id}.xml") - .toString() - - override fun login(username: String, password: String): Completable { - createHeaders(username, password) - return client.newCall(GET(getLoginUrl(), headers)) - .asObservable() - .doOnNext { it.close() } - .doOnNext { if (it.code() != 200) throw Exception("Login error") } - .toCompletable() - } - - fun search(query: String): Observable> { - return client.newCall(GET(getSearchUrl(query), headers)) - .asObservable() - .map { Jsoup.parse(it.body().string()) } - .flatMap { Observable.from(it.select("entry")) } - .filter { it.select("type").text() != "Novel" } - .map { - MangaSync.create(id).apply { - title = it.selectText("title")!! - remote_id = it.selectInt("id") - total_chapters = it.selectInt("chapters") - } - } - .toList() - } - - // MAL doesn't support score with decimals - fun getList(): Observable> { - return networkService.forceCacheClient - .newCall(GET(getListUrl(getUsername()), headers)) - .asObservable() - .map { Jsoup.parse(it.body().string()) } - .flatMap { Observable.from(it.select("manga")) } - .map { - MangaSync.create(id).apply { - title = it.selectText("series_title")!! - remote_id = it.selectInt("series_mangadb_id") - last_chapter_read = it.selectInt("my_read_chapters") - status = it.selectInt("my_status") - score = it.selectInt("my_score").toFloat() - total_chapters = it.selectInt("series_chapters") - } - } - .toList() - } - - override fun update(manga: MangaSync): Observable { - return Observable.defer { - if (manga.total_chapters != 0 && manga.last_chapter_read == manga.total_chapters) { - manga.status = COMPLETED - } - client.newCall(POST(getUpdateUrl(manga), headers, getMangaPostPayload(manga))) - .asObservable() - .doOnNext { it.close() } - .doOnNext { if (!it.isSuccessful) throw Exception("Could not update manga") } - .map { manga } - } - - } - - override fun add(manga: MangaSync): Observable { - return Observable.defer { - client.newCall(POST(getAddUrl(manga), headers, getMangaPostPayload(manga))) - .asObservable() - .doOnNext { it.close() } - .doOnNext { if (!it.isSuccessful) throw Exception("Could not add manga") } - .map { manga } - } - } - - private fun getMangaPostPayload(manga: MangaSync): RequestBody { - val xml = Xml.newSerializer() - val writer = StringWriter() - - with(xml) { - setOutput(writer) - startDocument("UTF-8", false) - startTag("", ENTRY_TAG) - - // Last chapter read - if (manga.last_chapter_read != 0) { - inTag(CHAPTER_TAG, manga.last_chapter_read.toString()) - } - // Manga status in the list - inTag(STATUS_TAG, manga.status.toString()) - - // Manga score - inTag(SCORE_TAG, manga.score.toString()) - - endTag("", ENTRY_TAG) - endDocument() - } - - val form = FormBody.Builder() - form.add("data", writer.toString()) - return form.build() - } - - fun XmlSerializer.inTag(tag: String, body: String, namespace: String = "") { - startTag(namespace, tag) - text(body) - endTag(namespace, tag) - } - - override fun bind(manga: MangaSync): Observable { - return getList() - .flatMap { userlist -> - manga.sync_id = id - val mangaFromList = userlist.find { it.remote_id == manga.remote_id } - if (mangaFromList != null) { - manga.copyPersonalFrom(mangaFromList) - update(manga) - } else { - // Set default fields if it's not found in the list - manga.score = DEFAULT_SCORE.toFloat() - manga.status = DEFAULT_STATUS - add(manga) - } - } - } - - override fun getStatus(status: Int): String = with(context) { - when (status) { - READING -> getString(R.string.reading) - COMPLETED -> getString(R.string.completed) - ON_HOLD -> getString(R.string.on_hold) - DROPPED -> getString(R.string.dropped) - PLAN_TO_READ -> getString(R.string.plan_to_read) - else -> "" - } - } - - fun createHeaders(username: String, password: String) { - val builder = Headers.Builder() - builder.add("Authorization", Credentials.basic(username, password)) - builder.add("User-Agent", "api-indiv-9F93C52A963974CF674325391990191C") - headers = builder.build() - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/network/CloudflareInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/network/CloudflareInterceptor.kt index 41b8a5aaf..503b779a3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/network/CloudflareInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/network/CloudflareInterceptor.kt @@ -60,8 +60,7 @@ class CloudflareInterceptor(private val cookies: PersistentCookieStore) : Interc .replace(Regex("""\s{3,}[a-z](?: = |\.).+"""), "") .replace("\n", "") - // Duktape can only return strings, so the result has to be converted to string first - val result = duktape.evaluate("$js.toString()").toInt() + val result = (duktape.evaluate(js) as Double).toInt() val answer = "${result + domain.length}" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt index 28b6fdc09..76e7932b2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt @@ -26,6 +26,10 @@ class PreferenceKeys(context: Context) { val customBrightnessValue = context.getString(R.string.pref_custom_brightness_value_key) + val colorFilter = context.getString(R.string.pref_color_filter_key) + + val colorFilterValue = context.getString(R.string.pref_color_filter_value_key) + val defaultViewer = context.getString(R.string.pref_default_viewer_key) val imageScaleType = context.getString(R.string.pref_image_scale_type_key) @@ -66,9 +70,7 @@ class PreferenceKeys(context: Context) { val downloadOnlyOverWifi = context.getString(R.string.pref_download_only_over_wifi_key) - val removeAfterRead = context.getString(R.string.pref_remove_after_read_key) - - val removeAfterReadPrevious = context.getString(R.string.pref_remove_after_read_previous_key) + val removeAfterReadSlots = context.getString(R.string.pref_remove_after_read_slots_key) val removeAfterMarkedAsRead = context.getString(R.string.pref_remove_after_marked_as_read_key) @@ -76,11 +78,13 @@ class PreferenceKeys(context: Context) { val libraryUpdateRestriction = context.getString(R.string.pref_library_update_restriction_key) + val libraryUpdateCategories = context.getString(R.string.pref_library_update_categories_key) + val filterDownloaded = context.getString(R.string.pref_filter_downloaded_key) val filterUnread = context.getString(R.string.pref_filter_unread_key) - val automaticUpdateStatus = context.getString(R.string.pref_enable_automatic_updates_key) + val automaticUpdates = context.getString(R.string.pref_enable_automatic_updates_key) val startScreen = context.getString(R.string.pref_start_screen_key) @@ -92,4 +96,6 @@ class PreferenceKeys(context: Context) { fun syncPassword(syncId: Int) = "pref_mangasync_password_$syncId" + val libraryAsList = context.getString(R.string.pref_display_library_as_list) + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index 4c77531ad..53fd03f43 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -52,6 +52,10 @@ class PreferencesHelper(context: Context) { fun customBrightnessValue() = rxPrefs.getInteger(keys.customBrightnessValue, 0) + fun colorFilter() = rxPrefs.getBoolean(keys.colorFilter, false) + + fun colorFilterValue() = rxPrefs.getInteger(keys.colorFilterValue, 0) + fun defaultViewer() = prefs.getInt(keys.defaultViewer, 1) fun imageScaleType() = rxPrefs.getInteger(keys.imageScaleType, 1) @@ -116,9 +120,7 @@ class PreferencesHelper(context: Context) { fun downloadOnlyOverWifi() = prefs.getBoolean(keys.downloadOnlyOverWifi, true) - fun removeAfterRead() = prefs.getBoolean(keys.removeAfterRead, false) - - fun removeAfterReadPrevious() = prefs.getBoolean(keys.removeAfterReadPrevious, false) + fun removeAfterReadSlots() = prefs.getInt(keys.removeAfterReadSlots, -1) fun removeAfterMarkedAsRead() = prefs.getBoolean(keys.removeAfterMarkedAsRead, false) @@ -126,10 +128,16 @@ class PreferencesHelper(context: Context) { fun libraryUpdateRestriction() = prefs.getStringSet(keys.libraryUpdateRestriction, emptySet()) + fun libraryUpdateCategories() = rxPrefs.getStringSet(keys.libraryUpdateCategories, emptySet()) + + fun libraryAsList() = rxPrefs.getBoolean(keys.libraryAsList, false) + fun filterDownloaded() = rxPrefs.getBoolean(keys.filterDownloaded, false) fun filterUnread() = rxPrefs.getBoolean(keys.filterUnread, false) - fun automaticUpdateStatus() = prefs.getBoolean(keys.automaticUpdateStatus, false) + fun automaticUpdates() = prefs.getBoolean(keys.automaticUpdates, false) + + fun hiddenCatalogues() = rxPrefs.getStringSet("hidden_catalogues", emptySet()) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/Source.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/Source.kt index d7fd5c5c1..ba196a51f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/Source.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/Source.kt @@ -47,5 +47,4 @@ interface Source { * @param page the page. */ fun fetchImage(page: Page): Observable - } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/SourceManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/SourceManager.kt index 01e1c9ddd..8bd3a2872 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/SourceManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/SourceManager.kt @@ -15,17 +15,7 @@ import java.io.File open class SourceManager(private val context: Context) { - val EHENTAI = 1 - val EXHENTAI = 2 - - val LAST_SOURCE by lazy { - if (DialogLogin.isLoggedIn(context, false)) - 2 - else - 1 - } - - val sourcesMap = createSources() + private val sourcesMap = createSources() open fun get(sourceKey: Int): Source? { return sourcesMap[sourceKey] @@ -33,16 +23,14 @@ open class SourceManager(private val context: Context) { fun getOnlineSources() = sourcesMap.values.filterIsInstance(OnlineSource::class.java) - private fun createSource(id: Int): Source? = when (id) { - EHENTAI -> EHentai(context, id, false) - EXHENTAI -> EHentai(context, id, true) - else -> null - } + private fun createOnlineSourceList(): List = + if (DialogLogin.isLoggedIn(context, false)) + listOf(EHentai(1, false), EHentai(2, true)) + else + listOf(EHentai(1, false)) private fun createSources(): Map = hashMapOf().apply { - for (i in 1..LAST_SOURCE) { - createSource(i)?.let { put(i, it) } - } + createOnlineSourceList().forEach { put(it.id, it) } val parsersDir = File(Environment.getExternalStorageDirectory().absolutePath + File.separator + context.getString(R.string.app_name), "parsers") @@ -52,7 +40,7 @@ open class SourceManager(private val context: Context) { for (file in parsersDir.listFiles().filter { it.extension == "yml" }) { try { val map = file.inputStream().use { yaml.loadAs(it, Map::class.java) } - YamlOnlineSource(context, map).let { put(it.id, it) } + YamlOnlineSource(map).let { put(it.id, it) } } catch (e: Exception) { Timber.e("Error loading source from file. Bad format?") } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/OnlineSource.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/OnlineSource.kt index 58fb64deb..4f6f7f79c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/OnlineSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/OnlineSource.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.data.source.online -import android.content.Context import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Manga @@ -23,10 +22,8 @@ import uy.kohesive.injekt.injectLazy /** * A simple implementation for sources from a website. - * - * @param context the application context. */ -abstract class OnlineSource(context: Context) : Source { +abstract class OnlineSource() : Source { /** * Network service. @@ -53,11 +50,21 @@ abstract class OnlineSource(context: Context) : Source { */ abstract val lang: Language + /** + * Whether the source has support for latest updates. + */ + abstract val supportsLatest : Boolean + /** * Headers used for requests. */ val headers by lazy { headersBuilder().build() } + /** + * Genre filters. + */ + val filters by lazy { getFilterList() } + /** * Default network client for doing requests. */ @@ -126,11 +133,11 @@ abstract class OnlineSource(context: Context) : Source { * the current page and the next page url. * @param query the search query. */ - open fun fetchSearchManga(page: MangasPage, query: String): Observable = client - .newCall(searchMangaRequest(page, query)) + open fun fetchSearchManga(page: MangasPage, query: String, filters: List): Observable = client + .newCall(searchMangaRequest(page, query, filters)) .asObservable() .map { response -> - searchMangaParse(response, page, query) + searchMangaParse(response, page, query, filters) page } @@ -141,9 +148,9 @@ abstract class OnlineSource(context: Context) : Source { * @param page the page object. * @param query the search query. */ - open protected fun searchMangaRequest(page: MangasPage, query: String): Request { + open protected fun searchMangaRequest(page: MangasPage, query: String, filters: List): Request { if (page.page == 1) { - page.url = searchMangaInitialUrl(query) + page.url = searchMangaInitialUrl(query, filters) } return GET(page.url, headers) } @@ -153,7 +160,7 @@ abstract class OnlineSource(context: Context) : Source { * * @param query the search query. */ - abstract protected fun searchMangaInitialUrl(query: String): String + abstract protected fun searchMangaInitialUrl(query: String, filters: List): String /** * Parse the response from the site. It should add a list of manga and the absolute url to the @@ -163,7 +170,38 @@ abstract class OnlineSource(context: Context) : Source { * @param page the page object to be filled. * @param query the search query. */ - abstract protected fun searchMangaParse(response: Response, page: MangasPage, query: String) + abstract protected fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List) + + /** + * Returns an observable containing a page with a list of latest manga. + */ + open fun fetchLatestUpdates(page: MangasPage): Observable = client + .newCall(latestUpdatesRequest(page)) + .asObservable() + .map { response -> + latestUpdatesParse(response, page) + page + } + + /** + * Returns the request for latest manga given the page. + */ + open protected fun latestUpdatesRequest(page: MangasPage): Request { + if (page.page == 1) { + page.url = latestUpdatesInitialUrl() + } + return GET(page.url, headers) + } + + /** + * Returns the absolute url of the first page to latest manga. + */ + abstract protected fun latestUpdatesInitialUrl(): String + + /** + * Same as [popularMangaParse], but for latest manga. + */ + abstract protected fun latestUpdatesParse(response: Response, page: MangasPage) /** * Returns an observable with the updated details for a manga. Normally it's not needed to @@ -187,7 +225,7 @@ abstract class OnlineSource(context: Context) : Source { * * @param manga the manga to be updated. */ - open protected fun mangaDetailsRequest(manga: Manga): Request { + open fun mangaDetailsRequest(manga: Manga): Request { return GET(baseUrl + manga.url, headers) } @@ -428,4 +466,7 @@ abstract class OnlineSource(context: Context) : Source { } + data class Filter(val id: String, val name: String) + + open fun getFilterList(): List = emptyList() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/ParsedOnlineSource.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/ParsedOnlineSource.kt index 2c2b8de14..6b0e21158 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/ParsedOnlineSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/ParsedOnlineSource.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.data.source.online -import android.content.Context import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.source.model.MangasPage @@ -12,10 +11,8 @@ import org.jsoup.nodes.Element /** * A simple implementation for sources from a website using Jsoup, an HTML parser. - * - * @param context the application context. */ -abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) { +abstract class ParsedOnlineSource() : OnlineSource() { /** * Parse the response from the site and fills [page]. @@ -64,7 +61,7 @@ abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) { * @param page the page object to be filled. * @param query the search query. */ - override fun searchMangaParse(response: Response, page: MangasPage, query: String) { + override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List) { val document = response.asJsoup() for (element in document.select(searchMangaSelector())) { Manga.create(id).apply { @@ -98,6 +95,38 @@ abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) { */ abstract protected fun searchMangaNextPageSelector(): String? + /** + * Parse the response from the site for latest updates and fills [page]. + */ + override fun latestUpdatesParse(response: Response, page: MangasPage) { + val document = response.asJsoup() + for (element in document.select(latestUpdatesSelector())) { + Manga.create(id).apply { + latestUpdatesFromElement(element, this) + page.mangas.add(this) + } + } + + latestUpdatesNextPageSelector()?.let { selector -> + page.nextPageUrl = document.select(selector).first()?.absUrl("href") + } + } + + /** + * Returns the Jsoup selector similar to [popularMangaSelector], but for latest updates. + */ + abstract protected fun latestUpdatesSelector(): String + + /** + * Fills [manga] with the given [element]. For latest updates. + */ + abstract protected fun latestUpdatesFromElement(element: Element, manga: Manga) + + /** + * Returns the Jsoup selector that returns the tag, like [popularMangaNextPageSelector]. + */ + abstract protected fun latestUpdatesNextPageSelector(): String? + /** * Parse the response from the site and fills the details of [manga]. * @@ -179,5 +208,4 @@ abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) { * @param document the parsed document. */ abstract protected fun imageUrlParse(document: Document): String - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/YamlOnlineSource.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/YamlOnlineSource.kt index 21f8c5919..b10f0fe10 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/YamlOnlineSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/YamlOnlineSource.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.data.source.online -import android.content.Context import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.network.GET @@ -17,7 +16,7 @@ import org.jsoup.nodes.Element import java.text.SimpleDateFormat import java.util.* -class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(context) { +class YamlOnlineSource(mappings: Map<*, *>) : OnlineSource() { val map = YamlSourceNode(mappings) @@ -32,6 +31,8 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con getLanguages().find { code == it.code }!! } + override val supportsLatest = map.latestupdates != null + override val client = when(map.client) { "cloudflare" -> network.cloudflareClient else -> network.client @@ -68,9 +69,9 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con } } - override fun searchMangaRequest(page: MangasPage, query: String): Request { + override fun searchMangaRequest(page: MangasPage, query: String, filters: List): Request { if (page.page == 1) { - page.url = searchMangaInitialUrl(query) + page.url = searchMangaInitialUrl(query, filters) } return when (map.search.method?.toLowerCase()) { "post" -> POST(page.url, headers, map.search.createForm()) @@ -78,9 +79,9 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con } } - override fun searchMangaInitialUrl(query: String) = map.search.url.replace("\$query", query) + override fun searchMangaInitialUrl(query: String, filters: List) = map.search.url.replace("\$query", query) - override fun searchMangaParse(response: Response, page: MangasPage, query: String) { + override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List) { val document = response.asJsoup() for (element in document.select(map.search.manga_css)) { Manga.create(id).apply { @@ -95,6 +96,33 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con } } + override fun latestUpdatesRequest(page: MangasPage): Request { + if (page.page == 1) { + page.url = latestUpdatesInitialUrl() + } + return when (map.latestupdates!!.method?.toLowerCase()) { + "post" -> POST(page.url, headers, map.latestupdates.createForm()) + else -> GET(page.url, headers) + } + } + + override fun latestUpdatesInitialUrl() = map.latestupdates!!.url + + override fun latestUpdatesParse(response: Response, page: MangasPage) { + val document = response.asJsoup() + for (element in document.select(map.latestupdates!!.manga_css)) { + Manga.create(id).apply { + title = element.text() + setUrlWithoutDomain(element.attr("href")) + page.mangas.add(this) + } + } + + map.latestupdates.next_url_css?.let { selector -> + page.nextPageUrl = document.select(selector).first()?.absUrl("href") + } + } + override fun mangaDetailsParse(response: Response, manga: Manga) { val document = response.asJsoup() with(map.manga) { @@ -184,5 +212,4 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con throw Exception("image_regex and image_css are null") } } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/YamlOnlineSourceMappings.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/YamlOnlineSourceMappings.kt index 970534de8..e4b643481 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/YamlOnlineSourceMappings.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/YamlOnlineSourceMappings.kt @@ -30,6 +30,8 @@ class YamlSourceNode(uncheckedMap: Map<*, *>) { val popular = PopularNode(toMap(map["popular"])!!) + val latestupdates = toMap(map["latest_updates"])?.let { LatestUpdatesNode(it) } + val search = SearchNode(toMap(map["search"])!!) val manga = MangaNode(toMap(map["manga"])!!) @@ -73,6 +75,17 @@ class PopularNode(override val map: Map): RequestableNode { } + +class LatestUpdatesNode(override val map: Map): RequestableNode { + + val manga_css: String by map + + val next_url_css: String? + get() = map["next_url_css"] as? String + +} + + class SearchNode(override val map: Map): RequestableNode { val manga_css: String by map diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubService.kt index 7bce4082b..42ff97324 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubService.kt @@ -6,7 +6,6 @@ import retrofit2.converter.gson.GsonConverterFactory import retrofit2.http.GET import rx.Observable - /** * Used to connect with the Github API. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubUpdateChecker.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubUpdateChecker.kt index 306fab71b..8d6210845 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubUpdateChecker.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubUpdateChecker.kt @@ -1,20 +1,25 @@ package eu.kanade.tachiyomi.data.updater -import android.content.Context -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.util.toast +import eu.kanade.tachiyomi.BuildConfig import rx.Observable +class GithubUpdateChecker() { -class GithubUpdateChecker(private val context: Context) { - - val service: GithubService = GithubService.create() + private val service: GithubService = GithubService.create() /** * Returns observable containing release information */ - fun checkForApplicationUpdate(): Observable { - context.toast(R.string.update_check_look_for_updates) - return service.getLatestVersion() + fun checkForUpdate(): Observable { + return service.getLatestVersion().map { release -> + val newVersion = release.version.replace("[^\\d.]".toRegex(), "") + + // Check if latest version is different from current version + if (newVersion != BuildConfig.VERSION_NAME) { + GithubUpdateResult.NewUpdate(release) + } else { + GithubUpdateResult.NoNewUpdate() + } + } } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubUpdateResult.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubUpdateResult.kt new file mode 100644 index 000000000..a4a89a1c0 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubUpdateResult.kt @@ -0,0 +1,7 @@ +package eu.kanade.tachiyomi.data.updater + +sealed class GithubUpdateResult { + + class NewUpdate(val release: GithubRelease): GithubUpdateResult() + class NoNewUpdate(): GithubUpdateResult() +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateCheckerService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateCheckerService.kt new file mode 100644 index 000000000..7386fc580 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateCheckerService.kt @@ -0,0 +1,80 @@ +package eu.kanade.tachiyomi.data.updater + +import android.content.Context +import android.support.v4.app.NotificationCompat +import com.google.android.gms.gcm.* +import eu.kanade.tachiyomi.Constants.NOTIFICATION_UPDATER_ID +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.util.notificationManager +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class UpdateCheckerService : GcmTaskService() { + + override fun onInitializeTasks() { + val preferences: PreferencesHelper = Injekt.get() + if (preferences.automaticUpdates()) { + setupTask(this) + } + } + + override fun onRunTask(params: TaskParams): Int { + return checkVersion() + } + + fun checkVersion(): Int { + return GithubUpdateChecker() + .checkForUpdate() + .map { result -> + if (result is GithubUpdateResult.NewUpdate) { + val url = result.release.downloadLink + + NotificationCompat.Builder(this).update { + setContentTitle(getString(R.string.app_name)) + setContentText(getString(R.string.update_check_notification_update_available)) + setSmallIcon(android.R.drawable.stat_sys_download_done) + // Download action + addAction(android.R.drawable.stat_sys_download_done, + getString(R.string.action_download), + UpdateNotificationReceiver.downloadApkIntent( + this@UpdateCheckerService, url)) + } + } + GcmNetworkManager.RESULT_SUCCESS + } + .onErrorReturn { GcmNetworkManager.RESULT_FAILURE } + // Sadly, the task needs to be synchronous. + .toBlocking() + .single() + } + + fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) { + block() + notificationManager.notify(NOTIFICATION_UPDATER_ID, build()) + } + + companion object { + fun setupTask(context: Context) { + val task = PeriodicTask.Builder() + .setService(UpdateCheckerService::class.java) + .setTag("Updater") + // 24 hours + .setPeriod(24 * 60 * 60) + // Run between the last two hours + .setFlex(2 * 60 * 60) + .setRequiredNetwork(Task.NETWORK_STATE_CONNECTED) + .setPersisted(true) + .setUpdateCurrent(true) + .build() + + GcmNetworkManager.getInstance(context).schedule(task) + } + + fun cancelTask(context: Context) { + GcmNetworkManager.getInstance(context).cancelAllTasks(UpdateCheckerService::class.java) + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloader.kt deleted file mode 100644 index 3dad020a7..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloader.kt +++ /dev/null @@ -1,202 +0,0 @@ -package eu.kanade.tachiyomi.data.updater - -import android.app.Notification -import android.app.PendingIntent -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.os.AsyncTask -import android.support.v4.app.NotificationCompat -import eu.kanade.tachiyomi.Constants -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.network.GET -import eu.kanade.tachiyomi.data.network.NetworkHelper -import eu.kanade.tachiyomi.data.network.ProgressListener -import eu.kanade.tachiyomi.data.network.newCallWithProgress -import eu.kanade.tachiyomi.util.notificationManager -import eu.kanade.tachiyomi.util.saveTo -import timber.log.Timber -import uy.kohesive.injekt.injectLazy -import java.io.File - -class UpdateDownloader(private val context: Context) : - AsyncTask() { - - companion object { - /** - * Prompt user with apk install intent - * @param context context - * @param file file of apk that is installed - */ - fun installAPK(context: Context, file: File) { - // Prompt install interface - val intent = Intent(Intent.ACTION_VIEW) - intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive") - // Without this flag android returned a intent error! - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - context.startActivity(intent) - } - } - - val network: NetworkHelper by injectLazy() - - /** - * Default download dir - */ - private val apkFile = File(context.externalCacheDir, "update.apk") - - - /** - * Notification builder - */ - private val notificationBuilder = NotificationCompat.Builder(context) - - /** - * Id of the notification - */ - private val notificationId: Int - get() = Constants.NOTIFICATION_UPDATER_ID - - - /** - * Class containing download result - * @param url url of file - * @param successful status of download - */ - class DownloadResult(var url: String, var successful: Boolean) - - /** - * Called before downloading - */ - override fun onPreExecute() { - // Create download notification - with(notificationBuilder) { - setContentTitle(context.getString(R.string.update_check_notification_file_download)) - setContentText(context.getString(R.string.update_check_notification_download_in_progress)) - setSmallIcon(android.R.drawable.stat_sys_download) - } - } - - override fun doInBackground(vararg params: String?): DownloadResult { - // Initialize information array containing path and url to file. - val result = DownloadResult(params[0]!!, false) - - // Progress of the download - var savedProgress = 0 - - val progressListener = object : ProgressListener { - override fun update(bytesRead: Long, contentLength: Long, done: Boolean) { - val progress = (100 * bytesRead / contentLength).toInt() - if (progress > savedProgress) { - savedProgress = progress - publishProgress(progress) - } - } - } - - try { - // Make the request and download the file - val response = network.client.newCallWithProgress(GET(result.url), progressListener).execute() - - if (response.isSuccessful) { - response.body().source().saveTo(apkFile) - // Set download successful - result.successful = true - } else { - response.close() - } - } catch (e: Exception) { - Timber.e(e, e.message) - } - - return result - } - - /** - * Called when progress is updated - * @param values values containing progress - */ - override fun onProgressUpdate(vararg values: Int?) { - // Notify notification manager to update notification - values.getOrNull(0)?.let { - notificationBuilder.setProgress(100, it, false) - // Displays the progress bar on notification - context.notificationManager.notify(notificationId, notificationBuilder.build()) - } - } - - /** - * Called when download done - * @param result string containing download information - */ - override fun onPostExecute(result: DownloadResult) { - with(notificationBuilder) { - if (result.successful) { - setContentTitle(context.getString(R.string.app_name)) - setContentText(context.getString(R.string.update_check_notification_download_complete)) - addAction(R.drawable.ic_system_update_grey_24dp_img, context.getString(R.string.action_install), - getInstallOnReceivedIntent(InstallOnReceived.INSTALL_APK, apkFile.absolutePath)) - addAction(R.drawable.ic_clear_grey_24dp_img, context.getString(R.string.action_cancel), - getInstallOnReceivedIntent(InstallOnReceived.CANCEL_NOTIFICATION)) - } else { - setContentText(context.getString(R.string.update_check_notification_download_error)) - addAction(R.drawable.ic_refresh_grey_24dp_img, context.getString(R.string.action_retry), - getInstallOnReceivedIntent(InstallOnReceived.RETRY_DOWNLOAD, result.url)) - addAction(R.drawable.ic_clear_grey_24dp_img, context.getString(R.string.action_cancel), - getInstallOnReceivedIntent(InstallOnReceived.CANCEL_NOTIFICATION)) - } - setSmallIcon(android.R.drawable.stat_sys_download_done) - setProgress(0, 0, false) - } - val notification = notificationBuilder.build() - notification.flags = Notification.FLAG_NO_CLEAR - context.notificationManager.notify(notificationId, notification) - } - - /** - * Returns broadcast intent - * @param action action name of broadcast intent - * @param path path of file | url of file - * @return broadcast intent - */ - fun getInstallOnReceivedIntent(action: String, path: String = ""): PendingIntent { - val intent = Intent(context, InstallOnReceived::class.java).apply { - this.action = action - putExtra(InstallOnReceived.FILE_LOCATION, path) - } - return PendingIntent.getBroadcast(context, 0, intent, 0) - } - - - /** - * BroadcastEvent used to install apk or retry download - */ - class InstallOnReceived : BroadcastReceiver() { - companion object { - // Install apk action - const val INSTALL_APK = "eu.kanade.INSTALL_APK" - - // Retry download action - const val RETRY_DOWNLOAD = "eu.kanade.RETRY_DOWNLOAD" - - // Retry download action - const val CANCEL_NOTIFICATION = "eu.kanade.CANCEL_NOTIFICATION" - - // Absolute path of file || URL of file - const val FILE_LOCATION = "file_location" - } - - override fun onReceive(context: Context, intent: Intent) { - when (intent.action) { - // Install apk. - INSTALL_APK -> UpdateDownloader.installAPK(context, File(intent.getStringExtra(FILE_LOCATION))) - // Retry download. - RETRY_DOWNLOAD -> UpdateDownloader(context).execute(intent.getStringExtra(FILE_LOCATION)) - - CANCEL_NOTIFICATION -> context.notificationManager.cancel(Constants.NOTIFICATION_UPDATER_ID) - } - } - - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderAlarm.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderAlarm.kt deleted file mode 100644 index 66e01deb9..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderAlarm.kt +++ /dev/null @@ -1,110 +0,0 @@ -package eu.kanade.tachiyomi.data.updater - -import android.app.AlarmManager -import android.app.PendingIntent -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.os.SystemClock -import eu.kanade.tachiyomi.BuildConfig -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.util.DeviceUtil -import eu.kanade.tachiyomi.util.alarmManager -import eu.kanade.tachiyomi.util.notification -import eu.kanade.tachiyomi.util.notificationManager -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get - -class UpdateDownloaderAlarm : BroadcastReceiver() { - - companion object { - const val CHECK_UPDATE_ACTION = "eu.kanade.CHECK_UPDATE" - - /** - * Sets the alarm to run the intent that checks for update - * @param context the application context. - * @param intervalInHours the time in hours when it will be executed. - */ - fun startAlarm(context: Context, intervalInHours: Int = 12, - isEnabled: Boolean = Injekt.get().automaticUpdateStatus()) { - // Stop previous running alarms if needed, and do not restart it if the interval is 0. - UpdateDownloaderAlarm.stopAlarm(context) - if (intervalInHours == 0 || !isEnabled) - return - - // Get the time the alarm should fire the event to update. - val intervalInMillis = intervalInHours * 60 * 60 * 1000 - val nextRun = SystemClock.elapsedRealtime() + intervalInMillis - - // Start the alarm. - val pendingIntent = getPendingIntent(context) - context.alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, - nextRun, intervalInMillis.toLong(), pendingIntent) - } - - /** - * Stops the alarm if it's running. - * @param context the application context. - */ - fun stopAlarm(context: Context) { - val pendingIntent = getPendingIntent(context) - context.alarmManager.cancel(pendingIntent) - } - - /** - * Returns broadcast intent - * @param context the application context. - * @return broadcast intent - */ - fun getPendingIntent(context: Context): PendingIntent { - return PendingIntent.getBroadcast(context, 0, - Intent(context, UpdateDownloaderAlarm::class.java).apply { - this.action = CHECK_UPDATE_ACTION - }, PendingIntent.FLAG_UPDATE_CURRENT) - } - } - - - override fun onReceive(context: Context, intent: Intent) { - when (intent.action) { - // Start the alarm when the system is booted. - Intent.ACTION_BOOT_COMPLETED -> startAlarm(context) - // Update the library when the alarm fires an event. - CHECK_UPDATE_ACTION -> checkVersion(context) - } - } - - fun checkVersion(context: Context) { - if (DeviceUtil.isNetworkConnected(context)) { - val updateChecker = GithubUpdateChecker(context) - updateChecker.checkForApplicationUpdate() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ release -> - //Get version of latest release - var newVersion = release.version - newVersion = newVersion.replace("[^\\d.]".toRegex(), "") - - //Check if latest version is different from current version - if (newVersion != BuildConfig.VERSION_NAME) { - val downloadLink = release.downloadLink - - val n = context.notification() { - setContentTitle(context.getString(R.string.update_check_notification_update_available)) - addAction(android.R.drawable.stat_sys_download_done, context.getString(eu.kanade.tachiyomi.R.string.action_download), - UpdateDownloader(context).getInstallOnReceivedIntent(UpdateDownloader.InstallOnReceived.RETRY_DOWNLOAD, downloadLink)) - setSmallIcon(android.R.drawable.stat_sys_download_done) - } - // Displays the progress bar on notification - context.notificationManager.notify(0, n); - } - }, { - it.printStackTrace() - }) - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderService.kt new file mode 100644 index 000000000..562277a30 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderService.kt @@ -0,0 +1,149 @@ +package eu.kanade.tachiyomi.data.updater + +import android.app.IntentService +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.support.v4.app.NotificationCompat +import eu.kanade.tachiyomi.Constants.NOTIFICATION_UPDATER_ID +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.network.GET +import eu.kanade.tachiyomi.data.network.NetworkHelper +import eu.kanade.tachiyomi.data.network.ProgressListener +import eu.kanade.tachiyomi.data.network.newCallWithProgress +import eu.kanade.tachiyomi.util.notificationManager +import eu.kanade.tachiyomi.util.saveTo +import timber.log.Timber +import uy.kohesive.injekt.injectLazy +import java.io.File + +class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.java.name) { + + companion object { + /** + * Download url. + */ + const val EXTRA_DOWNLOAD_URL = "eu.kanade.APP_DOWNLOAD_URL" + + /** + * Downloads a new update and let the user install the new version from a notification. + * @param context the application context. + * @param url the url to the new update. + */ + fun downloadUpdate(context: Context, url: String) { + val intent = Intent(context, UpdateDownloaderService::class.java).apply { + putExtra(EXTRA_DOWNLOAD_URL, url) + } + context.startService(intent) + } + + /** + * Prompt user with apk install intent + * @param context context + * @param file file of apk that is installed + */ + fun installAPK(context: Context, file: File) { + // Prompt install interface + val intent = Intent(Intent.ACTION_VIEW).apply { + setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive") + // Without this flag android returned a intent error! + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + context.startActivity(intent) + } + } + + /** + * Network helper + */ + private val network: NetworkHelper by injectLazy() + + override fun onHandleIntent(intent: Intent?) { + if (intent == null) return + + val url = intent.getStringExtra(EXTRA_DOWNLOAD_URL) ?: return + downloadApk(url) + } + + fun downloadApk(url: String) { + val progressNotification = NotificationCompat.Builder(this) + + progressNotification.update { + setContentTitle(getString(R.string.app_name)) + setContentText(getString(R.string.update_check_notification_download_in_progress)) + setSmallIcon(android.R.drawable.stat_sys_download) + setOngoing(true) + } + + // Progress of the download + var savedProgress = 0 + + val progressListener = object : ProgressListener { + override fun update(bytesRead: Long, contentLength: Long, done: Boolean) { + val progress = (100 * bytesRead / contentLength).toInt() + if (progress > savedProgress) { + savedProgress = progress + + progressNotification.update { setProgress(100, progress, false) } + } + } + } + + // Reference the context for later usage inside apply blocks. + val ctx = this + + try { + // Download the new update. + val response = network.client.newCallWithProgress(GET(url), progressListener).execute() + + // File where the apk will be saved + val apkFile = File(externalCacheDir, "update.apk") + + if (response.isSuccessful) { + response.body().source().saveTo(apkFile) + } else { + response.close() + throw Exception("Unsuccessful response") + } + + // Prompt the user to install the new update. + NotificationCompat.Builder(this).update { + setContentTitle(getString(R.string.app_name)) + setContentText(getString(R.string.update_check_notification_download_complete)) + setSmallIcon(android.R.drawable.stat_sys_download_done) + // Install action + addAction(R.drawable.ic_system_update_grey_24dp_img, + getString(R.string.action_install), + UpdateNotificationReceiver.installApkIntent(ctx, apkFile.absolutePath)) + // Cancel action + addAction(R.drawable.ic_clear_grey_24dp_img, + getString(R.string.action_cancel), + UpdateNotificationReceiver.cancelNotificationIntent(ctx)) + } + + } catch (error: Exception) { + Timber.e(error) + + // Prompt the user to retry the download. + NotificationCompat.Builder(this).update { + setContentTitle(getString(R.string.app_name)) + setContentText(getString(R.string.update_check_notification_download_error)) + setSmallIcon(android.R.drawable.stat_sys_download_done) + // Retry action + addAction(R.drawable.ic_refresh_grey_24dp_img, + getString(R.string.action_retry), + UpdateNotificationReceiver.downloadApkIntent(ctx, url)) + // Cancel action + addAction(R.drawable.ic_clear_grey_24dp_img, + getString(R.string.action_cancel), + UpdateNotificationReceiver.cancelNotificationIntent(ctx)) + } + } + } + + fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) { + block() + notificationManager.notify(NOTIFICATION_UPDATER_ID, build()) + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateNotificationReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateNotificationReceiver.kt new file mode 100644 index 000000000..cb8716115 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateNotificationReceiver.kt @@ -0,0 +1,67 @@ +package eu.kanade.tachiyomi.data.updater + +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import eu.kanade.tachiyomi.Constants.NOTIFICATION_UPDATER_ID +import eu.kanade.tachiyomi.util.notificationManager +import java.io.File + +class UpdateNotificationReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent) { + when (intent.action) { + ACTION_INSTALL_APK -> { + UpdateDownloaderService.installAPK(context, + File(intent.getStringExtra(EXTRA_FILE_LOCATION))) + cancelNotification(context) + } + ACTION_DOWNLOAD_UPDATE -> UpdateDownloaderService.downloadUpdate(context, + intent.getStringExtra(UpdateDownloaderService.EXTRA_DOWNLOAD_URL)) + ACTION_CANCEL_NOTIFICATION -> cancelNotification(context) + } + } + + fun cancelNotification(context: Context) { + context.notificationManager.cancel(NOTIFICATION_UPDATER_ID) + } + + companion object { + // Install apk action + const val ACTION_INSTALL_APK = "eu.kanade.INSTALL_APK" + + // Download apk action + const val ACTION_DOWNLOAD_UPDATE = "eu.kanade.RETRY_DOWNLOAD" + + // Cancel notification action + const val ACTION_CANCEL_NOTIFICATION = "eu.kanade.CANCEL_NOTIFICATION" + + // Absolute path of apk file + const val EXTRA_FILE_LOCATION = "file_location" + + fun cancelNotificationIntent(context: Context): PendingIntent { + val intent = Intent(context, UpdateNotificationReceiver::class.java).apply { + action = ACTION_CANCEL_NOTIFICATION + } + return PendingIntent.getBroadcast(context, 0, intent, 0) + } + + fun installApkIntent(context: Context, path: String): PendingIntent { + val intent = Intent(context, UpdateNotificationReceiver::class.java).apply { + action = ACTION_INSTALL_APK + putExtra(EXTRA_FILE_LOCATION, path) + } + return PendingIntent.getBroadcast(context, 0, intent, 0) + } + + fun downloadApkIntent(context: Context, url: String): PendingIntent { + val intent = Intent(context, UpdateNotificationReceiver::class.java).apply { + action = ACTION_DOWNLOAD_UPDATE + putExtra(UpdateDownloaderService.EXTRA_DOWNLOAD_URL, url) + } + return PendingIntent.getBroadcast(context, 0, intent, 0) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/backup/BackupFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/backup/BackupFragment.kt index 8f06b2cbc..d17235478 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/backup/BackupFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/backup/BackupFragment.kt @@ -41,6 +41,8 @@ class BackupFragment : BaseRxFragment() { } override fun onViewCreated(view: View, savedState: Bundle?) { + setToolbarTitle(getString(R.string.label_backup)) + (activity as ActivityMixin).requestPermissionsOnMarshmallow() subscriptions = SubscriptionList() @@ -121,9 +123,9 @@ class BackupFragment : BaseRxFragment() { .observeOn(AndroidSchedulers.mainThread()) .subscribe({ presenter.restoreBackup(it) - }, { - context.toast(it.message) - Timber.e(it, it.message) + }, { error -> + context.toast(error.message) + Timber.e(error) }) .apply { subscriptions.add(this) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/adapter/FlexibleViewHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/adapter/FlexibleViewHolder.kt index ed5e39e65..2fe0ddfd6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/adapter/FlexibleViewHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/adapter/FlexibleViewHolder.kt @@ -27,7 +27,7 @@ abstract class FlexibleViewHolder(view: View, return true } - protected fun toggleActivation() { + fun toggleActivation() { itemView.isActivated = adapter.isSelected(adapterPosition) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt index 278fd2737..f9c37e2b7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt @@ -2,13 +2,13 @@ package eu.kanade.tachiyomi.ui.catalogue import android.content.res.Configuration import android.os.Bundle +import android.support.design.widget.Snackbar import android.support.v7.widget.GridLayoutManager import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.SearchView import android.support.v7.widget.Toolbar import android.view.* import android.view.animation.AnimationUtils -import android.widget.AdapterView import android.widget.ArrayAdapter import android.widget.ProgressBar import android.widget.Spinner @@ -16,7 +16,7 @@ import com.afollestad.materialdialogs.MaterialDialog import com.f2prateek.rx.preferences.Preference import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.source.online.english.EHentai +import eu.kanade.tachiyomi.data.source.online.LoginSource import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment import eu.kanade.tachiyomi.ui.main.MainActivity @@ -26,6 +26,7 @@ import eu.kanade.tachiyomi.util.snack import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.widget.DividerItemDecoration import eu.kanade.tachiyomi.widget.EndlessScrollListener +import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener import kotlinx.android.synthetic.main.fragment_catalogue.* import kotlinx.android.synthetic.main.toolbar.* import nucleus.factory.RequiresPresenter @@ -40,12 +41,12 @@ import java.util.concurrent.TimeUnit.MILLISECONDS * Uses R.layout.fragment_catalogue. */ @RequiresPresenter(CataloguePresenter::class) -class CatalogueFragment : BaseRxFragment(), FlexibleViewHolder.OnListItemClickListener { +open class CatalogueFragment : BaseRxFragment(), FlexibleViewHolder.OnListItemClickListener { /** * Spinner shown in the toolbar to change the selected source. */ - private lateinit var spinner: Spinner + private var spinner: Spinner? = null /** * Adapter containing the list of manga from the catalogue. @@ -65,7 +66,7 @@ class CatalogueFragment : BaseRxFragment(), FlexibleViewHold /** * Query of the search box. */ - val query: String? + private val query: String get() = presenter.query /** @@ -93,11 +94,6 @@ class CatalogueFragment : BaseRxFragment(), FlexibleViewHold */ private var numColumnsSubscription: Subscription? = null - /** - * Display mode of the catalogue (list or grid mode). - */ - private var displayMode: MenuItem? = null - /** * Search item. */ @@ -130,6 +126,14 @@ class CatalogueFragment : BaseRxFragment(), FlexibleViewHold } override fun onViewCreated(view: View, savedState: Bundle?) { + // If the source list is empty or it only has unlogged sources, return to main screen. + val sources = presenter.sources + if (sources.isEmpty() || sources.all { it is LoginSource && !it.isLogged() }) { + context.toast(R.string.no_valid_sources) + activity.onBackPressed() + return + } + // Initialize adapter, scroll listener and recycler views adapter = CatalogueAdapter(this) @@ -145,7 +149,8 @@ class CatalogueFragment : BaseRxFragment(), FlexibleViewHold catalogue_list.adapter = adapter catalogue_list.layoutManager = llm catalogue_list.addOnScrollListener(listScrollListener) - catalogue_list.addItemDecoration(DividerItemDecoration(context.theme.getResourceDrawable(R.attr.divider_drawable))) + catalogue_list.addItemDecoration( + DividerItemDecoration(context.theme.getResourceDrawable(R.attr.divider_drawable))) if (presenter.isListMode) { switcher.showNext() @@ -167,28 +172,25 @@ class CatalogueFragment : BaseRxFragment(), FlexibleViewHold android.R.layout.simple_spinner_item, presenter.sources) spinnerAdapter.setDropDownViewResource(R.layout.spinner_item) - val onItemSelected = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) { - val source = spinnerAdapter.getItem(position) - if (!presenter.isValidSource(source)) { - spinner.setSelection(selectedIndex) - context.toast(R.string.source_requires_login) - } else if (source != presenter.source) { - selectedIndex = position - showProgressBar() - glm.scrollToPositionWithOffset(0, 0) - llm.scrollToPositionWithOffset(0, 0) - presenter.setActiveSource(source) - } - } - - override fun onNothingSelected(parent: AdapterView<*>) { + val onItemSelected = IgnoreFirstSpinnerListener { position -> + val source = spinnerAdapter.getItem(position) + if (!presenter.isValidSource(source)) { + spinner?.setSelection(selectedIndex) + context.toast(R.string.source_requires_login) + } else if (source != presenter.source) { + selectedIndex = position + showProgressBar() + glm.scrollToPositionWithOffset(0, 0) + llm.scrollToPositionWithOffset(0, 0) + presenter.setActiveSource(source) + activity.invalidateOptionsMenu() } } + selectedIndex = presenter.sources.indexOf(presenter.source) + spinner = Spinner(themedContext).apply { adapter = spinnerAdapter - selectedIndex = presenter.sources.indexOf(presenter.source) setSelection(selectedIndex) onItemSelectedListener = onItemSelected } @@ -206,39 +208,49 @@ class CatalogueFragment : BaseRxFragment(), FlexibleViewHold searchItem = menu.findItem(R.id.action_search).apply { val searchView = actionView as SearchView - if (!query.isNullOrEmpty()) { + if (!query.isBlank()) { expandActionView() searchView.setQuery(query, true) searchView.clearFocus() } searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String): Boolean { - onSearchEvent(query, true, false) + onSearchEvent(query, true) return true } override fun onQueryTextChange(newText: String): Boolean { - onSearchEvent(newText, false, false) + onSearchEvent(newText, false) return true } }) } + // Setup filters button + menu.findItem(R.id.action_set_filter).apply { + if (presenter.source.filters.isEmpty()) { + isEnabled = false + icon.alpha = 128 + } else { + isEnabled = true + icon.alpha = 255 + } + } + // Show next display mode - displayMode = menu.findItem(R.id.action_display_mode).apply { + menu.findItem(R.id.action_display_mode).apply { val icon = if (presenter.isListMode) R.drawable.ic_view_module_white_24dp else R.drawable.ic_view_list_white_24dp setIcon(icon) } - } override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.action_display_mode -> swapDisplayMode() - R.id.action_genre_filter -> EHentai.launchGenreSelectionDialog(context, this) + R.id.action_set_filter -> showFiltersDialog() else -> return super.onOptionsItemSelected(item) } return true @@ -248,7 +260,7 @@ class CatalogueFragment : BaseRxFragment(), FlexibleViewHold super.onResume() queryDebouncerSubscription = queryDebouncerSubject.debounce(SEARCH_TIMEOUT, MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) - .subscribe { searchWithQuery(it, false) } + .subscribe { searchWithQuery(it) } } override fun onPause() { @@ -261,7 +273,7 @@ class CatalogueFragment : BaseRxFragment(), FlexibleViewHold searchItem?.let { if (it.isActionViewExpanded) it.collapseActionView() } - toolbar.removeView(spinner) + spinner?.let { toolbar.removeView(it) } super.onDestroyView() } @@ -271,9 +283,9 @@ class CatalogueFragment : BaseRxFragment(), FlexibleViewHold * @param query the new query. * @param now whether to send the network call now or debounce it by [SEARCH_TIMEOUT]. */ - fun onSearchEvent(query: String, now: Boolean, forceRequest: Boolean) { + private fun onSearchEvent(query: String, now: Boolean) { if (now) { - searchWithQuery(query, forceRequest) + searchWithQuery(query) } else { queryDebouncerSubject.onNext(query) } @@ -284,9 +296,9 @@ class CatalogueFragment : BaseRxFragment(), FlexibleViewHold * * @param newQuery the new query. */ - private fun searchWithQuery(newQuery: String, forceRequest: Boolean) { + private fun searchWithQuery(newQuery: String) { // If text didn't change, do nothing - if (query == newQuery && !forceRequest) + if (query == newQuery) return showProgressBar() @@ -314,7 +326,7 @@ class CatalogueFragment : BaseRxFragment(), FlexibleViewHold */ fun onAddPage(page: Int, mangas: List) { hideProgressBar() - if (page == 0) { + if (page == 1) { adapter.clear() gridScrollListener.resetScroll() listScrollListener.resetScroll() @@ -329,12 +341,12 @@ class CatalogueFragment : BaseRxFragment(), FlexibleViewHold */ fun onAddPageError(error: Throwable) { hideProgressBar() - Timber.e(error, error.message) + Timber.e(error) - catalogue_view.snack(error.message ?: "") { + catalogue_view.snack(error.message ?: "", Snackbar.LENGTH_INDEFINITE) { setAction(R.string.action_retry) { showProgressBar() - presenter.retryPage() + presenter.requestNext() } } } @@ -354,11 +366,7 @@ class CatalogueFragment : BaseRxFragment(), FlexibleViewHold fun swapDisplayMode() { presenter.swapDisplayMode() val isListMode = presenter.isListMode - val icon = if (isListMode) - R.drawable.ic_view_module_white_24dp - else - R.drawable.ic_view_list_white_24dp - displayMode?.setIcon(icon) + activity.invalidateOptionsMenu() switcher.showNext() if (!isListMode) { // Initialize mangas if going to grid view @@ -446,4 +454,27 @@ class CatalogueFragment : BaseRxFragment(), FlexibleViewHold }.show() } + /** + * Show the filter dialog for the source. + */ + private fun showFiltersDialog() { + val allFilters = presenter.source.filters + val selectedFilters = presenter.filters + .map { filter -> allFilters.indexOf(filter) } + .toTypedArray() + + MaterialDialog.Builder(context) + .title(R.string.action_set_filter) + .items(allFilters.map { it.name }) + .itemsCallbackMultiChoice(selectedFilters) { dialog, positions, text -> + val newFilters = positions.map { allFilters[it] } + showProgressBar() + presenter.setSourceFilter(newFilters) + true + } + .positiveText(android.R.string.ok) + .negativeText(android.R.string.cancel) + .show() + } + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePager.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePager.kt new file mode 100644 index 000000000..301960414 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePager.kt @@ -0,0 +1,28 @@ +package eu.kanade.tachiyomi.ui.catalogue + +import eu.kanade.tachiyomi.data.source.model.MangasPage +import eu.kanade.tachiyomi.data.source.online.OnlineSource +import eu.kanade.tachiyomi.data.source.online.OnlineSource.Filter +import rx.Observable + +open class CataloguePager(val source: OnlineSource, val query: String, val filters: List): Pager() { + + override fun requestNext(transformer: (Observable) -> Observable): Observable { + val lastPage = lastPage + + val page = if (lastPage == null) + MangasPage(1) + else + MangasPage(lastPage.page + 1).apply { url = lastPage.nextPageUrl!! } + + val observable = if (query.isBlank() && filters.isEmpty()) + source.fetchPopularManga(page) + else + source.fetchSearchManga(page, query, filters) + + return transformer(observable) + .doOnNext { results.onNext(it) } + .doOnNext { this@CataloguePager.lastPage = it } + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt index 17fce8664..e34b09d91 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt @@ -12,19 +12,21 @@ import eu.kanade.tachiyomi.data.source.SourceManager import eu.kanade.tachiyomi.data.source.model.MangasPage import eu.kanade.tachiyomi.data.source.online.LoginSource import eu.kanade.tachiyomi.data.source.online.OnlineSource +import eu.kanade.tachiyomi.data.source.online.OnlineSource.Filter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.util.RxPager import rx.Observable +import rx.Subscription import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers import rx.subjects.PublishSubject import timber.log.Timber import uy.kohesive.injekt.injectLazy +import java.util.NoSuchElementException /** * Presenter of [CatalogueFragment]. */ -class CataloguePresenter : BasePresenter() { +open class CataloguePresenter : BasePresenter() { /** * Source manager. @@ -64,14 +66,14 @@ class CataloguePresenter : BasePresenter() { private set /** - * Pager containing a list of manga results. + * Active filters. */ - private var pager = RxPager() + var filters: List = emptyList() /** - * Last fetched page from network. + * Pager containing a list of manga results. */ - private var lastMangasPage: MangasPage? = null + private lateinit var pager: Pager /** * Subject that initializes a list of manga. @@ -84,80 +86,93 @@ class CataloguePresenter : BasePresenter() { var isListMode: Boolean = false private set - companion object { - /** - * Id of the restartable that delivers a list of manga. - */ - const val PAGER = 1 + /** + * Subscription for the pager. + */ + private var pagerSubscription: Subscription? = null - /** - * Id of the restartable that requests a page of manga from network. - */ - const val REQUEST_PAGE = 2 + /** + * Subscription for one request from the pager. + */ + private var pageSubscription: Subscription? = null - /** - * Id of the restartable that initializes the details of manga. - */ - const val GET_MANGA_DETAILS = 3 - - /** - * Key to save and restore [query] from a [Bundle]. - */ - const val QUERY_KEY = "query_key" - } + /** + * Subscription to initialize manga details. + */ + private var initializerSubscription: Subscription? = null override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) - source = getLastUsedSource() - - if (savedState != null) { - query = savedState.getString(QUERY_KEY, "") + try { + source = getLastUsedSource() + } catch (error: NoSuchElementException) { + return } - startableLatestCache(GET_MANGA_DETAILS, - { mangaDetailSubject.observeOn(Schedulers.io()) - .flatMap { Observable.from(it) } - .filter { !it.initialized } - .concatMap { getMangaDetailsObservable(it) } - .onBackpressureBuffer() - .observeOn(AndroidSchedulers.mainThread()) }, - { view, manga -> view.onMangaInitialized(manga) }, - { view, error -> Timber.e(error.message) }) + if (savedState != null) { + query = savedState.getString(CataloguePresenter::query.name, "") + } add(prefs.catalogueAsList().asObservable() .subscribe { setDisplayMode(it) }) - startableReplay(PAGER, - { pager.results() }, - { view, pair -> view.onAddPage(pair.first, pair.second) }) - - startableFirst(REQUEST_PAGE, - { pager.request { page -> getMangasPageObservable(page + 1) } }, - { view, next -> }, - { view, error -> view.onAddPageError(error) }) - - start(PAGER) - start(REQUEST_PAGE) + restartPager() } override fun onSave(state: Bundle) { - state.putString(QUERY_KEY, query) + state.putString(CataloguePresenter::query.name, query) super.onSave(state) } /** - * Sets the display mode. + * Restarts the pager for the active source with the provided query and filters. * - * @param asList whether the current mode is in list or not. + * @param query the query. + * @param filters the list of active filters (for search mode). */ - private fun setDisplayMode(asList: Boolean) { - isListMode = asList - if (asList) { - stop(GET_MANGA_DETAILS) - } else { - start(GET_MANGA_DETAILS) + fun restartPager(query: String = this.query, filters: List = this.filters) { + this.query = query + this.filters = filters + + if (!isListMode) { + subscribeToMangaInitializer() } + + // Create a new pager. + pager = createPager(query, filters) + + // Prepare the pager. + pagerSubscription?.let { remove(it) } + pagerSubscription = pager.results() + .subscribeReplay({ view, page -> + view.onAddPage(page.page, page.mangas) + }, { view, error -> + Timber.e(error) + }) + + // Request first page. + requestNext() + } + + /** + * Requests the next page for the active pager. + */ + fun requestNext() { + if (!hasNextPage()) return + + pageSubscription?.let { remove(it) } + pageSubscription = pager.requestNext { getPageTransformer(it) } + .subscribeFirst({ view, page -> + // Nothing to do when onNext is emitted. + }, CatalogueFragment::onAddPageError) + } + + /** + * Returns true if the last fetched page has a next page. + */ + fun hasNextPage(): Boolean { + return pager.hasNextPage() } /** @@ -168,73 +183,64 @@ class CataloguePresenter : BasePresenter() { fun setActiveSource(source: OnlineSource) { prefs.lastUsedCatalogueSource().set(source.id) this.source = source - restartPager() + + restartPager(query = "", filters = emptyList()) } /** - * Restarts the request for the active source. + * Sets the display mode. * - * @param query the query, or null if searching popular manga. + * @param asList whether the current mode is in list or not. */ - fun restartPager(query: String = "") { - this.query = query - stop(REQUEST_PAGE) - lastMangasPage = null - - if (!isListMode) { - start(GET_MANGA_DETAILS) - } - start(PAGER) - start(REQUEST_PAGE) - } - - /** - * Requests the next page for the active pager. - */ - fun requestNext() { - if (hasNextPage()) { - start(REQUEST_PAGE) + private fun setDisplayMode(asList: Boolean) { + isListMode = asList + if (asList) { + initializerSubscription?.let { remove(it) } + } else { + subscribeToMangaInitializer() } } /** - * Returns true if the last fetched page has a next page. + * Subscribes to the initializer of manga details and updates the view if needed. */ - fun hasNextPage(): Boolean { - return lastMangasPage?.nextPageUrl != null - } - - /** - * Retries the current request that failed. - */ - fun retryPage() { - start(REQUEST_PAGE) - } - - /** - * Returns the observable of the network request for a page. - * - * @param page the page number to request. - * @return an observable of the network request. - */ - private fun getMangasPageObservable(page: Int): Observable> { - val nextMangasPage = MangasPage(page) - if (page != 1) { - nextMangasPage.url = lastMangasPage!!.nextPageUrl!! - } - - val observable = if (query.isEmpty()) - source.fetchPopularManga(nextMangasPage) - else - source.fetchSearchManga(nextMangasPage, query) - - return observable.subscribeOn(Schedulers.io()) - .doOnNext { lastMangasPage = it } - .flatMap { Observable.from(it.mangas) } - .map { networkToLocalManga(it) } - .toList() - .doOnNext { initializeMangas(it) } + private fun subscribeToMangaInitializer() { + initializerSubscription?.let { remove(it) } + initializerSubscription = mangaDetailSubject.observeOn(Schedulers.io()) + .flatMap { Observable.from(it) } + .filter { !it.initialized } + .concatMap { getMangaDetailsObservable(it) } + .onBackpressureBuffer() .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ manga -> + @Suppress("DEPRECATION") + view?.onMangaInitialized(manga) + }, { error -> + Timber.e(error) + }) + .apply { add(this) } + } + + /** + * Returns the function to apply to the observable of the list of manga from the source. + * + * @param observable the observable from the source. + * @return the function to apply. + */ + fun getPageTransformer(observable: Observable): Observable { + return observable.subscribeOn(Schedulers.io()) + .doOnNext { it.mangas.replace { networkToLocalManga(it) } } + .doOnNext { initializeMangas(it.mangas) } + .observeOn(AndroidSchedulers.mainThread()) + } + + /** + * Replaces an object in the list with another. + */ + fun MutableList.replace(block: (T) -> T) { + forEachIndexed { i, obj -> + set(i, block(obj)) + } } /** @@ -299,7 +305,7 @@ class CataloguePresenter : BasePresenter() { * @param source the source to check. * @return true if the source is valid, false otherwise. */ - fun isValidSource(source: Source?): Boolean { + open fun isValidSource(source: Source?): Boolean { if (source == null) return false if (source is LoginSource) { @@ -321,8 +327,9 @@ class CataloguePresenter : BasePresenter() { /** * Returns a list of enabled sources ordered by language and name. */ - private fun getEnabledSources(): List { + open protected fun getEnabledSources(): List { val languages = prefs.enabledLanguages().getOrDefault() + val hiddenCatalogues = prefs.hiddenCatalogues().getOrDefault() // Ensure at least one language if (languages.isEmpty()) { @@ -331,6 +338,7 @@ class CataloguePresenter : BasePresenter() { return sourceManager.getOnlineSources() .filter { it.lang.code in languages } + .filterNot { it.id.toString() in hiddenCatalogues } .sortedBy { "(${it.lang.code}) ${it.name}" } } @@ -354,4 +362,17 @@ class CataloguePresenter : BasePresenter() { prefs.catalogueAsList().set(!isListMode) } + /** + * Set the active filters for the current source. + * + * @param selectedFilters a list of active filters. + */ + fun setSourceFilter(selectedFilters: List) { + restartPager(filters = selectedFilters) + } + + open fun createPager(query: String, filters: List): Pager { + return CataloguePager(source, query, filters) + } + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/Pager.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/Pager.kt new file mode 100644 index 000000000..26cb466f6 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/Pager.kt @@ -0,0 +1,25 @@ +package eu.kanade.tachiyomi.ui.catalogue + +import eu.kanade.tachiyomi.data.source.model.MangasPage +import rx.subjects.PublishSubject +import rx.Observable + +/** + * A general pager for source requests (latest updates, popular, search) + */ +abstract class Pager { + + protected var lastPage: MangasPage? = null + + protected val results = PublishSubject.create() + + fun results(): Observable { + return results.asObservable() + } + + fun hasNextPage(): Boolean { + return lastPage == null || lastPage?.nextPageUrl != null + } + + abstract fun requestNext(transformer: (Observable) -> Observable): Observable +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryActivity.kt index 8a01bcacf..44d295b76 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryActivity.kt @@ -55,9 +55,9 @@ class CategoryActivity : BaseRxActivity(), ActionMode.Callbac } } - override fun onCreate(savedInstanceState: Bundle?) { + override fun onCreate(savedState: Bundle?) { setAppTheme() - super.onCreate(savedInstanceState) + super.onCreate(savedState) // Inflate activity_edit_categories.xml. setContentView(R.layout.activity_edit_categories) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadPresenter.kt index ca6b31269..d02582a5c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadPresenter.kt @@ -35,7 +35,7 @@ class DownloadPresenter : BasePresenter() { .subscribeLatestCache({ view, downloads -> view.onNextDownloads(downloads) }, { view, error -> - Timber.e(error, error.message) + Timber.e(error) }) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesFragment.kt new file mode 100644 index 000000000..cff2ea3db --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesFragment.kt @@ -0,0 +1,29 @@ +package eu.kanade.tachiyomi.ui.latest_updates + +import eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment +import nucleus.factory.RequiresPresenter +import android.view.* +import eu.kanade.tachiyomi.R + +/** + * Fragment that shows the manga from the catalogue. Inherit CatalogueFragment. + */ +@RequiresPresenter(LatestUpdatesPresenter::class) +class LatestUpdatesFragment : CatalogueFragment() { + + override fun onPrepareOptionsMenu(menu: Menu) { + super.onPrepareOptionsMenu(menu) + menu.findItem(R.id.action_search).isVisible = false + menu.findItem(R.id.action_set_filter).isVisible = false + + } + + companion object { + + fun newInstance(): LatestUpdatesFragment { + return LatestUpdatesFragment() + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPager.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPager.kt new file mode 100644 index 000000000..6391f9d7c --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPager.kt @@ -0,0 +1,28 @@ +package eu.kanade.tachiyomi.ui.latest_updates + +import eu.kanade.tachiyomi.data.source.model.MangasPage +import eu.kanade.tachiyomi.data.source.online.OnlineSource +import eu.kanade.tachiyomi.ui.catalogue.Pager +import rx.Observable + +/** + * LatestUpdatesPager inherited from the general Pager. + */ +class LatestUpdatesPager(val source: OnlineSource): Pager() { + + override fun requestNext(transformer: (Observable) -> Observable): Observable { + val lastPage = lastPage + + val page = if (lastPage == null) + MangasPage(1) + else + MangasPage(lastPage.page + 1).apply { url = lastPage.nextPageUrl!! } + + val observable = source.fetchLatestUpdates(page) + + return transformer(observable) + .doOnNext { results.onNext(it) } + .doOnNext { this@LatestUpdatesPager.lastPage = it } + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt new file mode 100644 index 000000000..8df1196e6 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt @@ -0,0 +1,26 @@ +package eu.kanade.tachiyomi.ui.latest_updates + +import eu.kanade.tachiyomi.data.source.Source +import eu.kanade.tachiyomi.data.source.online.OnlineSource +import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter +import eu.kanade.tachiyomi.ui.catalogue.Pager +import eu.kanade.tachiyomi.data.source.online.OnlineSource.Filter + +/** + * Presenter of [LatestUpdatesFragment]. Inherit CataloguePresenter. + */ +class LatestUpdatesPresenter : CataloguePresenter() { + + override fun createPager(query: String, filters: List): Pager { + return LatestUpdatesPager(source) + } + + override fun getEnabledSources(): List { + return super.getEnabledSources().filter { it.supportsLatest } + } + + override fun isValidSource(source: Source?): Boolean { + return super.isValidSource(source) && (source as OnlineSource).supportsLatest + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.kt index dcfc8539e..fe4433ead 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.kt @@ -1,23 +1,23 @@ package eu.kanade.tachiyomi.ui.library -import android.support.v4.app.Fragment -import android.support.v4.app.FragmentManager - +import android.view.View +import android.view.ViewGroup +import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Category -import eu.kanade.tachiyomi.ui.base.adapter.SmartFragmentStatePagerAdapter +import eu.kanade.tachiyomi.util.inflate +import eu.kanade.tachiyomi.widget.RecyclerViewPagerAdapter /** * This adapter stores the categories from the library, used with a ViewPager. * - * @param fm the fragment manager. * @constructor creates an instance of the adapter. */ -class LibraryAdapter(fm: FragmentManager) : SmartFragmentStatePagerAdapter(fm) { +class LibraryAdapter(private val fragment: LibraryFragment) : RecyclerViewPagerAdapter() { /** * The categories to bind in the adapter. */ - var categories: List? = null + var categories: List = emptyList() // This setter helps to not refresh the adapter if the reference to the list doesn't change. set(value) { if (field !== value) { @@ -27,13 +27,34 @@ class LibraryAdapter(fm: FragmentManager) : SmartFragmentStatePagerAdapter(fm) { } /** - * Creates a new fragment for the given position when it's called. + * Creates a new view for this adapter. * - * @param position the position to instantiate. - * @return a fragment for the given position. + * @return a new view. */ - override fun getItem(position: Int): Fragment { - return LibraryCategoryFragment.newInstance(position) + override fun createView(container: ViewGroup): View { + val view = container.inflate(R.layout.item_library_category) as LibraryCategoryView + view.onCreate(fragment) + return view + } + + /** + * Binds a view with a position. + * + * @param view the view to bind. + * @param position the position in the adapter. + */ + override fun bindView(view: View, position: Int) { + (view as LibraryCategoryView).onBind(categories[position]) + } + + /** + * Recycles a view. + * + * @param view the view to recycle. + * @param position the position in the adapter. + */ + override fun recycleView(view: View, position: Int) { + (view as LibraryCategoryView).onRecycle() } /** @@ -42,7 +63,7 @@ class LibraryAdapter(fm: FragmentManager) : SmartFragmentStatePagerAdapter(fm) { * @return the number of categories or 0 if the list is null. */ override fun getCount(): Int { - return categories?.size ?: 0 + return categories.size } /** @@ -52,28 +73,16 @@ class LibraryAdapter(fm: FragmentManager) : SmartFragmentStatePagerAdapter(fm) { * @return the title to display. */ override fun getPageTitle(position: Int): CharSequence { - return categories!![position].name + return categories[position].name } /** - * Method to enable or disable the action mode (multiple selection) for all the instantiated - * fragments. - * - * @param mode the mode to set. + * Returns the position of the view. */ - fun setSelectionMode(mode: Int) { - for (fragment in getRegisteredFragments()) { - (fragment as LibraryCategoryFragment).setSelectionMode(mode) - } - } - - /** - * Notifies the adapters in all the registered fragments to refresh their content. - */ - fun refreshRegisteredAdapters() { - for (fragment in getRegisteredFragments()) { - (fragment as LibraryCategoryFragment).adapter.notifyDataSetChanged() - } + override fun getItemPosition(obj: Any?): Int { + val view = obj as? LibraryCategoryView ?: return POSITION_NONE + val index = categories.indexOfFirst { it.id == view.category.id } + return if (index == -1) POSITION_NONE else index } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt index c16e86af3..1a67ee3a3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt @@ -8,7 +8,7 @@ import eu.davidea.flexibleadapter.FlexibleAdapter import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.util.inflate -import kotlinx.android.synthetic.main.fragment_library_category.* +import eu.kanade.tachiyomi.widget.AutofitRecyclerView import kotlinx.android.synthetic.main.item_catalogue_grid.view.* import java.util.* @@ -17,7 +17,7 @@ import java.util.* * * @param fragment the fragment containing this adapter. */ -class LibraryCategoryAdapter(val fragment: LibraryCategoryFragment) : +class LibraryCategoryAdapter(val fragment: LibraryCategoryView) : FlexibleAdapter() { /** @@ -84,11 +84,18 @@ class LibraryCategoryAdapter(val fragment: LibraryCategoryFragment) : * @return a new view holder for a manga. */ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LibraryHolder { - val view = parent.inflate(R.layout.item_catalogue_grid).apply { - card.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight) - gradient.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight / 2, Gravity.BOTTOM) + // Depending on preferences, display a list or display a grid + if (parent is AutofitRecyclerView) { + val view = parent.inflate(R.layout.item_catalogue_grid).apply { + val coverHeight = parent.itemWidth / 3 * 4 + card.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight) + gradient.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight / 2, Gravity.BOTTOM) + } + return LibraryGridHolder(view, this, fragment) + } else { + val view = parent.inflate(R.layout.item_library_list) + return LibraryListHolder(view, this, fragment) } - return LibraryHolder(view, this, fragment) } /** @@ -101,14 +108,17 @@ class LibraryCategoryAdapter(val fragment: LibraryCategoryFragment) : val manga = getItem(position) holder.onSetValues(manga) - //When user scrolls this bind the correct selection status + // When user scrolls this bind the correct selection status holder.itemView.isActivated = isSelected(position) } /** - * Property to return the height for the covers based on the width to keep an aspect ratio. + * Returns the position in the adapter for the given manga. + * + * @param manga the manga to find. */ - val coverHeight: Int - get() = fragment.recycler.itemWidth / 3 * 4 + fun indexOf(manga: Manga): Int { + return mangas.orEmpty().indexOfFirst { it.id == manga.id } + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryFragment.kt deleted file mode 100644 index 7cf99f807..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryFragment.kt +++ /dev/null @@ -1,277 +0,0 @@ -package eu.kanade.tachiyomi.ui.library - -import android.content.res.Configuration -import android.os.Bundle -import android.support.v7.widget.LinearLayoutManager -import android.support.v7.widget.RecyclerView -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import com.f2prateek.rx.preferences.Preference -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.library.LibraryUpdateService -import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder -import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment -import eu.kanade.tachiyomi.ui.manga.MangaActivity -import eu.kanade.tachiyomi.util.toast -import kotlinx.android.synthetic.main.fragment_library_category.* -import rx.Subscription - -/** - * Fragment containing the library manga for a certain category. - * Uses R.layout.fragment_library_category. - */ -class LibraryCategoryFragment : BaseFragment(), FlexibleViewHolder.OnListItemClickListener { - - /** - * Adapter to hold the manga in this category. - */ - lateinit var adapter: LibraryCategoryAdapter - private set - - /** - * Position in the adapter from [LibraryAdapter]. - */ - private var position: Int = 0 - - /** - * Subscription for the library manga. - */ - private var libraryMangaSubscription: Subscription? = null - - /** - * Subscription of the number of manga per row. - */ - private var numColumnsSubscription: Subscription? = null - - /** - * Subscription of the library search. - */ - private var searchSubscription: Subscription? = null - - companion object { - /** - * Key to save and restore [position] from a [Bundle]. - */ - const val POSITION_KEY = "position_key" - - /** - * Creates a new instance of this class. - * - * @param position the position in the adapter from [LibraryAdapter]. - * @return a new instance of [LibraryCategoryFragment]. - */ - fun newInstance(position: Int): LibraryCategoryFragment { - val fragment = LibraryCategoryFragment() - fragment.position = position - return fragment - } - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_library_category, container, false) - } - - override fun onViewCreated(view: View, savedState: Bundle?) { - adapter = LibraryCategoryAdapter(this) - recycler.setHasFixedSize(true) - recycler.adapter = adapter - - if (libraryFragment.actionMode != null) { - setSelectionMode(FlexibleAdapter.MODE_MULTI) - } - - numColumnsSubscription = getColumnsPreferenceForCurrentOrientation().asObservable() - .doOnNext { recycler.spanCount = it } - .skip(1) - // Set again the adapter to recalculate the covers height - .subscribe { recycler.adapter = adapter } - - searchSubscription = libraryPresenter.searchSubject.subscribe { text -> - adapter.searchText = text - adapter.updateDataSet() - } - - if (savedState != null) { - position = savedState.getInt(POSITION_KEY) - adapter.onRestoreInstanceState(savedState) - - if (adapter.mode == FlexibleAdapter.MODE_SINGLE) { - adapter.clearSelection() - } - } - - recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() { - override fun onScrollStateChanged(recycler: RecyclerView, newState: Int) { - // Disable swipe refresh when view is not at the top - val firstPos = (recycler.layoutManager as LinearLayoutManager) - .findFirstCompletelyVisibleItemPosition() - swipe_refresh.isEnabled = firstPos == 0 - } - }) - - // Double the distance required to trigger sync - swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt()) - swipe_refresh.setOnRefreshListener { - if (!LibraryUpdateService.isRunning(activity)) { - libraryPresenter.categories.getOrNull(position)?.let { - LibraryUpdateService.start(activity, true, it) - context.toast(R.string.updating_category) - } - } - // It can be a very long operation, so we disable swipe refresh and show a toast. - swipe_refresh.isRefreshing = false - } - } - - override fun onDestroyView() { - numColumnsSubscription?.unsubscribe() - searchSubscription?.unsubscribe() - super.onDestroyView() - } - - override fun onResume() { - super.onResume() - libraryMangaSubscription = libraryPresenter.libraryMangaSubject - .subscribe { onNextLibraryManga(it) } - } - - override fun onPause() { - libraryMangaSubscription?.unsubscribe() - super.onPause() - } - - override fun onSaveInstanceState(outState: Bundle) { - outState.putInt(POSITION_KEY, position) - adapter.onSaveInstanceState(outState) - super.onSaveInstanceState(outState) - } - - /** - * Subscribe to [LibraryMangaEvent]. When an event is received, it updates the content of the - * adapter. - * - * @param event the event received. - */ - fun onNextLibraryManga(event: LibraryMangaEvent) { - // Get the categories from the parent fragment. - val categories = libraryFragment.adapter.categories ?: return - - // When a category is deleted, the index can be greater than the number of categories. - if (position >= categories.size) return - - // Get the manga list for this category. - val mangaForCategory = event.getMangaForCategory(categories[position]) ?: emptyList() - - // Update the category with its manga. - adapter.setItems(mangaForCategory) - } - - /** - * Called when a manga is clicked. - * - * @param position the position of the element clicked. - * @return true if the item should be selected, false otherwise. - */ - override fun onListItemClick(position: Int): Boolean { - // If the action mode is created and the position is valid, toggle the selection. - val item = adapter.getItem(position) ?: return false - if (libraryFragment.actionMode != null) { - toggleSelection(position) - return true - } else { - openManga(item) - return false - } - } - - /** - * Called when a manga is long clicked. - * - * @param position the position of the element clicked. - */ - override fun onListItemLongClick(position: Int) { - libraryFragment.createActionModeIfNeeded() - toggleSelection(position) - } - - /** - * Opens a manga. - * - * @param manga the manga to open. - */ - protected fun openManga(manga: Manga) { - // Notify the presenter a manga is being opened. - libraryPresenter.onOpenManga() - - // Create a new activity with the manga. - val intent = MangaActivity.newIntent(activity, manga) - startActivity(intent) - } - - /** - * Toggles the selection for a manga. - * - * @param position the position to toggle. - */ - private fun toggleSelection(position: Int) { - val library = libraryFragment - - // Toggle the selection. - adapter.toggleSelection(position, false) - - // Notify the selection to the presenter. - library.presenter.setSelection(adapter.getItem(position), adapter.isSelected(position)) - - // Get the selected count. - val count = library.presenter.selectedMangas.size - if (count == 0) { - // Destroy action mode if there are no items selected. - library.destroyActionModeIfNeeded() - } else { - // Update action mode with the new selection. - library.setContextTitle(count) - library.setVisibilityOfCoverEdit(count) - library.invalidateActionMode() - } - } - - /** - * Returns a preference for the number of manga per row based on the current orientation. - * - * @return the preference. - */ - fun getColumnsPreferenceForCurrentOrientation(): Preference { - return if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) - libraryPresenter.preferences.portraitColumns() - else - libraryPresenter.preferences.landscapeColumns() - } - - /** - * Sets the mode for the adapter. - * - * @param mode the mode to set. It should be MODE_SINGLE or MODE_MULTI. - */ - fun setSelectionMode(mode: Int) { - adapter.mode = mode - if (mode == FlexibleAdapter.MODE_SINGLE) { - adapter.clearSelection() - } - } - - /** - * Property to get the library fragment. - */ - private val libraryFragment: LibraryFragment - get() = parentFragment as LibraryFragment - - /** - * Property to get the library presenter. - */ - private val libraryPresenter: LibraryPresenter - get() = libraryFragment.presenter - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt new file mode 100644 index 000000000..3caece0ef --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt @@ -0,0 +1,266 @@ +package eu.kanade.tachiyomi.ui.library + +import android.content.Context +import android.support.v7.widget.LinearLayoutManager +import android.support.v7.widget.RecyclerView +import android.util.AttributeSet +import android.widget.FrameLayout +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Category +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.library.LibraryUpdateService +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder +import eu.kanade.tachiyomi.ui.manga.MangaActivity +import eu.kanade.tachiyomi.util.inflate +import eu.kanade.tachiyomi.util.toast +import eu.kanade.tachiyomi.widget.AutofitRecyclerView +import kotlinx.android.synthetic.main.item_library_category.view.* +import rx.Subscription +import uy.kohesive.injekt.injectLazy + +/** + * Fragment containing the library manga for a certain category. + * Uses R.layout.fragment_library_category. + */ +class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) +: FrameLayout(context, attrs), FlexibleViewHolder.OnListItemClickListener { + + /** + * Preferences. + */ + private val preferences: PreferencesHelper by injectLazy() + + /** + * The fragment containing this view. + */ + private lateinit var fragment: LibraryFragment + + /** + * Category for this view. + */ + lateinit var category: Category + private set + + /** + * Recycler view of the list of manga. + */ + private lateinit var recycler: RecyclerView + + /** + * Adapter to hold the manga in this category. + */ + private lateinit var adapter: LibraryCategoryAdapter + + /** + * Subscription for the library manga. + */ + private var libraryMangaSubscription: Subscription? = null + + /** + * Subscription of the library search. + */ + private var searchSubscription: Subscription? = null + + /** + * Subscription of the library selections. + */ + private var selectionSubscription: Subscription? = null + + fun onCreate(fragment: LibraryFragment) { + this.fragment = fragment + + recycler = if (preferences.libraryAsList().getOrDefault()) { + (swipe_refresh.inflate(R.layout.library_list_recycler) as RecyclerView).apply { + layoutManager = LinearLayoutManager(context) + } + } else { + (swipe_refresh.inflate(R.layout.library_grid_recycler) as AutofitRecyclerView).apply { + spanCount = fragment.mangaPerRow + } + } + + adapter = LibraryCategoryAdapter(this) + + recycler.setHasFixedSize(true) + recycler.adapter = adapter + swipe_refresh.addView(recycler) + + recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrollStateChanged(recycler: RecyclerView, newState: Int) { + // Disable swipe refresh when view is not at the top + val firstPos = (recycler.layoutManager as LinearLayoutManager) + .findFirstCompletelyVisibleItemPosition() + swipe_refresh.isEnabled = firstPos == 0 + } + }) + + // Double the distance required to trigger sync + swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt()) + swipe_refresh.setOnRefreshListener { + if (!LibraryUpdateService.isRunning(context)) { + LibraryUpdateService.start(context, category) + context.toast(R.string.updating_category) + } + // It can be a very long operation, so we disable swipe refresh and show a toast. + swipe_refresh.isRefreshing = false + } + } + + fun onBind(category: Category) { + this.category = category + + val presenter = fragment.presenter + + searchSubscription = presenter.searchSubject.subscribe { text -> + adapter.searchText = text + adapter.updateDataSet() + } + + adapter.mode = if (presenter.selectedMangas.isNotEmpty()) { + FlexibleAdapter.MODE_MULTI + } else { + FlexibleAdapter.MODE_SINGLE + } + + libraryMangaSubscription = presenter.libraryMangaSubject + .subscribe { onNextLibraryManga(it) } + + selectionSubscription = presenter.selectionSubject + .subscribe { onSelectionChanged(it) } + } + + fun onRecycle() { + adapter.setItems(emptyList()) + adapter.clearSelection() + } + + override fun onDetachedFromWindow() { + searchSubscription?.unsubscribe() + libraryMangaSubscription?.unsubscribe() + selectionSubscription?.unsubscribe() + super.onDetachedFromWindow() + } + + /** + * Subscribe to [LibraryMangaEvent]. When an event is received, it updates the content of the + * adapter. + * + * @param event the event received. + */ + fun onNextLibraryManga(event: LibraryMangaEvent) { + // Get the manga list for this category. + val mangaForCategory = event.getMangaForCategory(category).orEmpty() + + // Update the category with its manga. + adapter.setItems(mangaForCategory) + + if (adapter.mode == FlexibleAdapter.MODE_MULTI) { + fragment.presenter.selectedMangas.forEach { manga -> + val position = adapter.indexOf(manga) + if (position != -1 && !adapter.isSelected(position)) { + adapter.toggleSelection(position) + (recycler.findViewHolderForItemId(manga.id!!) as? LibraryHolder)?.toggleActivation() + } + } + } + } + + /** + * Subscribe to [LibrarySelectionEvent]. When an event is received, it updates the selection + * depending on the type of event received. + * + * @param event the selection event received. + */ + private fun onSelectionChanged(event: LibrarySelectionEvent) { + when (event) { + is LibrarySelectionEvent.Selected -> { + if (adapter.mode != FlexibleAdapter.MODE_MULTI) { + adapter.mode = FlexibleAdapter.MODE_MULTI + } + findAndToggleSelection(event.manga) + } + is LibrarySelectionEvent.Unselected -> { + findAndToggleSelection(event.manga) + if (fragment.presenter.selectedMangas.isEmpty()) { + adapter.mode = FlexibleAdapter.MODE_SINGLE + } + } + is LibrarySelectionEvent.Cleared -> { + adapter.mode = FlexibleAdapter.MODE_SINGLE + adapter.clearSelection() + } + } + } + + /** + * Toggles the selection for the given manga and updates the view if needed. + * + * @param manga the manga to toggle. + */ + private fun findAndToggleSelection(manga: Manga) { + val position = adapter.indexOf(manga) + if (position != -1) { + adapter.toggleSelection(position) + (recycler.findViewHolderForItemId(manga.id!!) as? LibraryHolder)?.toggleActivation() + } + } + + /** + * Called when a manga is clicked. + * + * @param position the position of the element clicked. + * @return true if the item should be selected, false otherwise. + */ + override fun onListItemClick(position: Int): Boolean { + // If the action mode is created and the position is valid, toggle the selection. + val item = adapter.getItem(position) ?: return false + if (adapter.mode == FlexibleAdapter.MODE_MULTI) { + toggleSelection(position) + return true + } else { + openManga(item) + return false + } + } + + /** + * Called when a manga is long clicked. + * + * @param position the position of the element clicked. + */ + override fun onListItemLongClick(position: Int) { + fragment.createActionModeIfNeeded() + toggleSelection(position) + } + + /** + * Opens a manga. + * + * @param manga the manga to open. + */ + private fun openManga(manga: Manga) { + // Notify the presenter a manga is being opened. + fragment.presenter.onOpenManga() + + // Create a new activity with the manga. + val intent = MangaActivity.newIntent(context, manga) + fragment.startActivity(intent) + } + + + /** + * Tells the presenter to toggle the selection for the given position. + * + * @param position the position to toggle. + */ + private fun toggleSelection(position: Int) { + val manga = adapter.getItem(position) ?: return + + fragment.presenter.setSelection(manga, !adapter.isSelected(position)) + fragment.invalidateActionMode() + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt index c948f18ab..6253922b7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.library import android.app.Activity import android.content.Intent +import android.content.res.Configuration import android.os.Bundle import android.support.design.widget.TabLayout import android.support.v4.view.ViewPager @@ -9,12 +10,13 @@ import android.support.v7.view.ActionMode import android.support.v7.widget.SearchView import android.view.* import com.afollestad.materialdialogs.MaterialDialog -import eu.davidea.flexibleadapter.FlexibleAdapter +import com.f2prateek.rx.preferences.Preference import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.library.LibraryUpdateService +import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.source.online.english.EHentai import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment @@ -25,6 +27,9 @@ import exh.FavoritesSyncManager import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.fragment_library.* import nucleus.factory.RequiresPresenter +import rx.Subscription +import timber.log.Timber +import uy.kohesive.injekt.injectLazy import java.io.IOException /** @@ -40,6 +45,11 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback lateinit var adapter: LibraryAdapter private set + /** + * Preferences. + */ + val preferences: PreferencesHelper by injectLazy() + /** * TabLayout of the categories. */ @@ -59,8 +69,7 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback /** * Action mode for manga selection. */ - var actionMode: ActionMode? = null - private set + private var actionMode: ActionMode? = null /** * Selected manga for editing its cover. @@ -79,6 +88,17 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback lateinit var favoritesSyncManager: FavoritesSyncManager + /** + * Number of manga per row in grid mode. + */ + var mangaPerRow = 0 + private set + + /** + * Subscription for the number of manga per row. + */ + private var numColumnsSubscription: Subscription? = null + companion object { /** * Key to change the cover of a manga in [onActivityResult]. @@ -108,8 +128,8 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) setHasOptionsMenu(true) - isFilterDownloaded = presenter.preferences.filterDownloaded().get() as Boolean - isFilterUnread = presenter.preferences.filterUnread().get() as Boolean + isFilterDownloaded = preferences.filterDownloaded().get() as Boolean + isFilterUnread = preferences.filterUnread().get() as Boolean favoritesSyncManager = FavoritesSyncManager(context, DatabaseHelper(context)) } @@ -120,11 +140,11 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback override fun onViewCreated(view: View, savedState: Bundle?) { setToolbarTitle(getString(R.string.label_library)) - adapter = LibraryAdapter(childFragmentManager) + adapter = LibraryAdapter(this) view_pager.adapter = adapter view_pager.addOnPageChangeListener(object : ViewPager.SimpleOnPageChangeListener() { override fun onPageSelected(position: Int) { - presenter.preferences.lastUsedCategory().set(position) + preferences.lastUsedCategory().set(position) } }) tabs.setupWithViewPager(view_pager) @@ -133,9 +153,18 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback activeCategory = savedState.getInt(CATEGORY_KEY) query = savedState.getString(QUERY_KEY) presenter.searchSubject.onNext(query) + if (presenter.selectedMangas.isNotEmpty()) { + createActionModeIfNeeded() + } } else { - activeCategory = presenter.preferences.lastUsedCategory().getOrDefault() + activeCategory = preferences.lastUsedCategory().getOrDefault() } + + numColumnsSubscription = getColumnsPreferenceForCurrentOrientation().asObservable() + .doOnNext { mangaPerRow = it } + .skip(1) + // Set again the adapter to recalculate the covers height + .subscribe { reattachAdapter() } } override fun onResume() { @@ -144,6 +173,7 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback } override fun onDestroyView() { + numColumnsSubscription?.unsubscribe() tabs.setupWithViewPager(null) tabs.visibility = View.GONE super.onDestroyView() @@ -184,6 +214,7 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback return true } }) + } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -192,7 +223,7 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback // Change unread filter status. isFilterUnread = !isFilterUnread // Update settings. - presenter.preferences.filterUnread().set(isFilterUnread) + preferences.filterUnread().set(isFilterUnread) // Apply filter. onFilterCheckboxChanged() } @@ -200,7 +231,7 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback // Change downloaded filter status. isFilterDownloaded = !isFilterDownloaded // Update settings. - presenter.preferences.filterDownloaded().set(isFilterDownloaded) + preferences.filterDownloaded().set(isFilterDownloaded) // Apply filter. onFilterCheckboxChanged() } @@ -209,14 +240,14 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback isFilterUnread = false isFilterDownloaded = false // Update settings. - presenter.preferences.filterUnread().set(isFilterUnread) - presenter.preferences.filterDownloaded().set(isFilterDownloaded) + preferences.filterUnread().set(isFilterUnread) + preferences.filterDownloaded().set(isFilterDownloaded) // Apply filter onFilterCheckboxChanged() } -// R.id.action_update_library -> { -// LibraryUpdateService.start(activity, true) -// } + R.id.action_library_display_mode -> swapDisplayMode() + //R.id.action_update_library -> { + // LibraryUpdateService.start(activity) R.id.action_sync -> { favoritesSyncManager.guiSyncFavorites({ (activity as MainActivity).setFragment(LibraryFragment.newInstance(), 0) @@ -236,12 +267,41 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback * Applies filter change */ private fun onFilterCheckboxChanged() { - presenter.updateLibrary() - adapter.notifyDataSetChanged() - adapter.refreshRegisteredAdapters() + presenter.resubscribeLibrary() activity.supportInvalidateOptionsMenu() } + /** + * Swap display mode + */ + private fun swapDisplayMode() { + presenter.swapDisplayMode() + reattachAdapter() + } + + /** + * Reattaches the adapter to the view pager to recreate fragments + */ + private fun reattachAdapter() { + val position = view_pager.currentItem + adapter.recycle = false + view_pager.adapter = adapter + view_pager.currentItem = position + adapter.recycle = true + } + + /** + * Returns a preference for the number of manga per row based on the current orientation. + * + * @return the preference. + */ + private fun getColumnsPreferenceForCurrentOrientation(): Preference { + return if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) + preferences.portraitColumns() + else + preferences.landscapeColumns() + } + /** * Updates the query. * @@ -268,7 +328,7 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback R.string.information_empty_library, R.drawable.ic_book_black_128dp) // Get the current active category. - val activeCat = if (adapter.categories != null) view_pager.currentItem else activeCategory + val activeCat = if (adapter.categories.isNotEmpty()) view_pager.currentItem else activeCategory // Set the categories adapter.categories = categories @@ -284,31 +344,42 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback } /** - * Sets the title of the action mode. - * - * @param count the number of items selected. + * Creates the action mode if it's not created already. */ - fun setContextTitle(count: Int) { - actionMode?.title = getString(R.string.label_selected, count) + fun createActionModeIfNeeded() { + if (actionMode == null) { + actionMode = activity.startSupportActionMode(this) + } } /** - * Sets the visibility of the edit cover item. - * - * @param count the number of items selected. + * Destroys the action mode. */ - fun setVisibilityOfCoverEdit(count: Int) { - // If count = 1 display edit button - actionMode?.menu?.findItem(R.id.action_edit_cover)?.isVisible = count == 1 + fun destroyActionModeIfNeeded() { + actionMode?.finish() + } + + /** + * Invalidates the action mode, forcing it to refresh its content. + */ + fun invalidateActionMode() { + actionMode?.invalidate() } override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { mode.menuInflater.inflate(R.menu.library_selection, menu) - adapter.setSelectionMode(FlexibleAdapter.MODE_MULTI) return true } override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { + val count = presenter.selectedMangas.size + if (count == 0) { + // Destroy action mode if there are no items selected. + destroyActionModeIfNeeded() + } else { + mode.title = getString(R.string.label_selected, count) + menu.findItem(R.id.action_edit_cover)?.isVisible = count == 1 + } return false } @@ -327,18 +398,10 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback } override fun onDestroyActionMode(mode: ActionMode) { - adapter.setSelectionMode(FlexibleAdapter.MODE_SINGLE) - presenter.selectedMangas.clear() + presenter.clearSelections() actionMode = null } - /** - * Destroys the action mode. - */ - fun destroyActionModeIfNeeded() { - actionMode?.finish() - } - /** * Changes the cover for the selected manga. * @@ -368,14 +431,14 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback context.contentResolver.openInputStream(data.data).use { // Update cover to selected file, show error if something went wrong if (presenter.editCoverWithStream(it, manga)) { - adapter.refreshRegisteredAdapters() + // TODO refresh cover } else { context.toast(R.string.notification_manga_update_failed) } } - } catch (e: IOException) { + } catch (error: IOException) { context.toast(R.string.notification_manga_update_failed) - e.printStackTrace() + Timber.e(error) } } @@ -422,20 +485,4 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback .show() } - /** - * Creates the action mode if it's not created already. - */ - fun createActionModeIfNeeded() { - if (actionMode == null) { - actionMode = activity.startSupportActionMode(this) - } - } - - /** - * Invalidates the action mode, forcing it to refresh its content. - */ - fun invalidateActionMode() { - actionMode?.invalidate() - } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt new file mode 100644 index 000000000..91424f3bd --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt @@ -0,0 +1,49 @@ +package eu.kanade.tachiyomi.ui.library + +import android.view.View +import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.DiskCacheStrategy +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder +import kotlinx.android.synthetic.main.item_catalogue_grid.view.* + +/** + * Class used to hold the displayed data of a manga in the library, like the cover or the title. + * All the elements from the layout file "item_catalogue_grid" are available in this class. + * + * @param view the inflated view for this holder. + * @param adapter the adapter handling this holder. + * @param listener a listener to react to single tap and long tap events. + * @constructor creates a new library holder. + */ +class LibraryGridHolder(private val view: View, + private val adapter: LibraryCategoryAdapter, + listener: FlexibleViewHolder.OnListItemClickListener) +: LibraryHolder(view, adapter, listener) { + + /** + * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this + * holder with the given manga. + * + * @param manga the manga to bind. + */ + override fun onSetValues(manga: Manga) { + // Update the title of the manga. + view.title.text = manga.title + + // Update the unread count and its visibility. + with(view.unread_text) { + visibility = if (manga.unread > 0) View.VISIBLE else View.GONE + text = manga.unread.toString() + } + + // Update the cover. + Glide.clear(view.thumbnail) + Glide.with(view.context) + .load(manga) + .diskCacheStrategy(DiskCacheStrategy.RESULT) + .centerCrop() + .into(view.thumbnail) + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt index 9cf28ae46..efdd42200 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt @@ -1,24 +1,19 @@ package eu.kanade.tachiyomi.ui.library import android.view.View -import com.bumptech.glide.Glide -import com.bumptech.glide.load.engine.DiskCacheStrategy import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder -import kotlinx.android.synthetic.main.item_catalogue_grid.view.* /** - * Class used to hold the displayed data of a manga in the library, like the cover or the title. - * All the elements from the layout file "item_catalogue_grid" are available in this class. - * + * Generic class used to hold the displayed data of a manga in the library. * @param view the inflated view for this holder. * @param adapter the adapter handling this holder. - * @param listener a listener to react to single tap and long tap events. - * @constructor creates a new library holder. + * @param listener a listener to react to the single tap and long tap events. */ -class LibraryHolder(private val view: View, - private val adapter: LibraryCategoryAdapter, - listener: FlexibleViewHolder.OnListItemClickListener) + +abstract class LibraryHolder(private val view: View, + adapter: LibraryCategoryAdapter, + listener: FlexibleViewHolder.OnListItemClickListener) : FlexibleViewHolder(view, adapter, listener) { /** @@ -27,23 +22,6 @@ class LibraryHolder(private val view: View, * * @param manga the manga to bind. */ - fun onSetValues(manga: Manga) { - // Update the title of the manga. - view.title.text = manga.title - - // Update the unread count and its visibility. - with(view.unread_text) { - visibility = if (manga.unread > 0) View.VISIBLE else View.GONE - text = manga.unread.toString() - } - - // Update the cover. - Glide.clear(view.thumbnail) - Glide.with(view.context) - .load(manga) - .diskCacheStrategy(DiskCacheStrategy.RESULT) - .centerCrop() - .into(view.thumbnail) - } + abstract fun onSetValues(manga: Manga) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt new file mode 100644 index 000000000..d0928262b --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt @@ -0,0 +1,57 @@ +package eu.kanade.tachiyomi.ui.library + +import android.view.View +import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.DiskCacheStrategy +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder +import kotlinx.android.synthetic.main.item_library_list.view.* + +/** + * Class used to hold the displayed data of a manga in the library, like the cover or the title. + * All the elements from the layout file "item_library_list" are available in this class. + * + * @param view the inflated view for this holder. + * @param adapter the adapter handling this holder. + * @param listener a listener to react to single tap and long tap events. + * @constructor creates a new library holder. + */ + +class LibraryListHolder(private val view: View, + private val adapter: LibraryCategoryAdapter, + listener: FlexibleViewHolder.OnListItemClickListener) +: LibraryHolder(view, adapter, listener) { + + /** + * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this + * holder with the given manga. + * + * @param manga the manga to bind. + */ + override fun onSetValues(manga: Manga) { + // Update the title of the manga. + itemView.title.text = manga.title + + // Update the unread count and its visibility. + with(itemView.unread_text) { + visibility = if (manga.unread > 0) View.VISIBLE else View.GONE + text = manga.unread.toString() + } + + // Create thumbnail onclick to simulate long click + itemView.thumbnail.setOnClickListener { + // Simulate long click on this view to enter selection mode + onLongClick(itemView) + } + + // Update the cover. + Glide.clear(itemView.thumbnail) + Glide.with(itemView.context) + .load(manga) + .diskCacheStrategy(DiskCacheStrategy.RESULT) + .centerCrop() + .dontAnimate() + .into(itemView.thumbnail) + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt index 83c8682c8..51a81cc9e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt @@ -16,6 +16,7 @@ import rx.Observable import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers import rx.subjects.BehaviorSubject +import rx.subjects.PublishSubject import uy.kohesive.injekt.injectLazy import java.io.IOException import java.io.InputStream @@ -29,22 +30,27 @@ class LibraryPresenter : BasePresenter() { /** * Categories of the library. */ - lateinit var categories: List + var categories: List = emptyList() /** * Currently selected manga. */ - var selectedMangas = mutableListOf() + val selectedMangas = mutableListOf() /** * Search query of the library. */ - val searchSubject = BehaviorSubject.create() + val searchSubject: BehaviorSubject = BehaviorSubject.create() /** * Subject to notify the library's viewpager for updates. */ - val libraryMangaSubject = BehaviorSubject.create() + val libraryMangaSubject: BehaviorSubject = BehaviorSubject.create() + + /** + * Subject to notify the UI of selection updates. + */ + val selectionSubject: PublishSubject = PublishSubject.create() /** * Database. @@ -149,7 +155,7 @@ class LibraryPresenter : BasePresenter() { /** * Resubscribes to library. */ - fun updateLibrary() { + fun resubscribeLibrary() { start(GET_LIBRARY) } @@ -219,17 +225,27 @@ class LibraryPresenter : BasePresenter() { fun setSelection(manga: Manga, selected: Boolean) { if (selected) { selectedMangas.add(manga) + selectionSubject.onNext(LibrarySelectionEvent.Selected(manga)) } else { selectedMangas.remove(manga) + selectionSubject.onNext(LibrarySelectionEvent.Unselected(manga)) } } + /** + * Clears all the manga selections and notifies the UI. + */ + fun clearSelections() { + selectedMangas.clear() + selectionSubject.onNext(LibrarySelectionEvent.Cleared()) + } + /** * Returns the common categories for the given list of manga. * * @param mangas the list of manga. */ - fun getCommonCategories(mangas: List) = mangas.toSet() + fun getCommonCategories(mangas: List): Collection = mangas.toSet() .map { db.getCategoriesForManga(it).executeAsBlocking() } .reduce { set1: Iterable, set2 -> set1.intersect(set2) } @@ -285,4 +301,12 @@ class LibraryPresenter : BasePresenter() { return false } + /** + * Changes the active display mode. + */ + fun swapDisplayMode() { + val displayAsList = preferences.libraryAsList().getOrDefault() + preferences.libraryAsList().set(!displayAsList) + } + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySelectionEvent.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySelectionEvent.kt new file mode 100644 index 000000000..e490e4364 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySelectionEvent.kt @@ -0,0 +1,10 @@ +package eu.kanade.tachiyomi.ui.library + +import eu.kanade.tachiyomi.data.database.models.Manga + +sealed class LibrarySelectionEvent { + + class Selected(val manga: Manga) : LibrarySelectionEvent() + class Unselected(val manga: Manga) : LibrarySelectionEvent() + class Cleared() : LibrarySelectionEvent() +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index 333944283..787eda73c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.ui.backup.BackupFragment import eu.kanade.tachiyomi.ui.base.activity.BaseActivity import eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment +import eu.kanade.tachiyomi.ui.latest_updates.LatestUpdatesFragment import eu.kanade.tachiyomi.ui.download.DownloadFragment import eu.kanade.tachiyomi.ui.library.LibraryFragment import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersFragment @@ -60,9 +61,10 @@ class MainActivity : BaseActivity() { val id = item.itemId when (id) { R.id.nav_drawer_library -> setFragment(LibraryFragment.newInstance(), id) - R.id.nav_drawer_recently_read -> setFragment(RecentlyReadFragment.newInstance(), id) R.id.nav_drawer_recent_updates -> setFragment(RecentChaptersFragment.newInstance(), id) + R.id.nav_drawer_recently_read -> setFragment(RecentlyReadFragment.newInstance(), id) R.id.nav_drawer_catalogues -> setFragment(CatalogueFragment.newInstance(), id) + R.id.nav_drawer_latest_updates -> setFragment(LatestUpdatesFragment.newInstance(), id) R.id.nav_drawer_downloads -> setFragment(DownloadFragment.newInstance(), id) R.id.nav_drawer_settings -> { val intent = Intent(this, SettingsActivity::class.java) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaActivity.kt index 2ace5ef93..387efa85a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaActivity.kt @@ -23,6 +23,7 @@ class MangaActivity : BaseRxActivity() { const val FROM_CATALOGUE_EXTRA = "from_catalogue" const val MANGA_EXTRA = "manga" + const val FROM_LAUNCHER_EXTRA = "from_launcher" const val INFO_FRAGMENT = 0 const val CHAPTERS_FRAGMENT = 1 @@ -45,6 +46,11 @@ class MangaActivity : BaseRxActivity() { super.onCreate(savedState) setContentView(R.layout.activity_manga) + val fromLauncher = intent.getBooleanExtra(FROM_LAUNCHER_EXTRA, false) + + //Remove any current manga if we are launching from launcher + if(fromLauncher) SharedData.remove(MangaEvent::class.java) + presenter.setMangaEvent(SharedData.getOrPut(MangaEvent::class.java) { val id = intent.getLongExtra(MANGA_EXTRA, 0) MangaEvent(presenter.db.getManga(id).executeAsBlocking()!!) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersFragment.kt index 2353f8c3b..318beec43 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersFragment.kt @@ -116,8 +116,25 @@ class ChaptersFragment : BaseRxFragment(), ActionMode.Callbac override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { inflater.inflate(R.menu.chapters, menu) - menu.findItem(R.id.action_filter_unread).isChecked = presenter.onlyUnread() - menu.findItem(R.id.action_filter_downloaded).isChecked = presenter.onlyDownloaded() + } + + override fun onPrepareOptionsMenu(menu: Menu) { + // Initialize menu items. + val menuFilterRead = menu.findItem(R.id.action_filter_read) + val menuFilterUnread = menu.findItem(R.id.action_filter_unread) + val menuFilterDownloaded = menu.findItem(R.id.action_filter_downloaded) + + // Set correct checkbox values. + menuFilterRead.isChecked = presenter.onlyRead() + menuFilterUnread.isChecked = presenter.onlyUnread() + menuFilterDownloaded.isChecked = presenter.onlyDownloaded() + + if (presenter.onlyRead()) + //Disable unread filter option if read filter is enabled. + menuFilterUnread.isEnabled = false + if (presenter.onlyUnread()) + //Disable read filter option if unread filter is enabled. + menuFilterRead.isEnabled = false } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -126,8 +143,14 @@ class ChaptersFragment : BaseRxFragment(), ActionMode.Callbac R.id.manga_download -> showDownloadDialog() R.id.action_sorting_mode -> showSortingDialog() R.id.action_filter_unread -> { + item.isChecked = !item.isChecked + presenter.setUnreadFilter(item.isChecked) + activity.supportInvalidateOptionsMenu() + } + R.id.action_filter_read -> { item.isChecked = !item.isChecked presenter.setReadFilter(item.isChecked) + activity.supportInvalidateOptionsMenu() } R.id.action_filter_downloaded -> { item.isChecked = !item.isChecked @@ -145,8 +168,7 @@ class ChaptersFragment : BaseRxFragment(), ActionMode.Callbac fun onNextManga(manga: Manga) { // Set initial values - setReadFilter() - setDownloadedFilter() + activity.supportInvalidateOptionsMenu() } fun onNextChapters(chapters: List) { @@ -242,6 +264,7 @@ class ChaptersFragment : BaseRxFragment(), ActionMode.Callbac fun getUnreadChaptersSorted() = presenter.chapters .filter { !it.read && !it.isDownloaded } + .distinctBy { it.name } .sortedByDescending { it.source_order } // i = 0: Download 1 @@ -354,7 +377,7 @@ class ChaptersFragment : BaseRxFragment(), ActionMode.Callbac fun onChaptersDeletedError(error: Throwable) { dismissDeletingDialog() - Timber.e(error, error.message) + Timber.e(error) } fun dismissDeletingDialog() { @@ -394,12 +417,4 @@ class ChaptersFragment : BaseRxFragment(), ActionMode.Callbac private fun setContextTitle(count: Int) { actionMode?.title = getString(R.string.label_selected, count) } - - fun setReadFilter() { - activity.supportInvalidateOptionsMenu() - } - - fun setDownloadedFilter() { - activity.supportInvalidateOptionsMenu() - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt index 402f15623..df212d13f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt @@ -111,7 +111,7 @@ class ChaptersPresenter : BasePresenter() { startableLatestCache(CHAPTER_STATUS_CHANGES, { getChapterStatusObservable() }, { view, download -> view.onChapterStatusChange(download) }, - { view, error -> Timber.e(error.cause, error.message) }) + { view, error -> Timber.e(error) }) // Find the active manga from the shared data or return. manga = SharedData.get(MangaEvent::class.java)?.manga ?: return @@ -209,6 +209,9 @@ class ChaptersPresenter : BasePresenter() { if (onlyUnread()) { observable = observable.filter { !it.read } } + if (onlyRead()) { + observable = observable.filter { it.read } + } if (onlyDownloaded()) { observable = observable.filter { it.isDownloaded } } @@ -349,12 +352,23 @@ class ChaptersPresenter : BasePresenter() { * * @param onlyUnread whether to display only unread chapters or all chapters. */ - fun setReadFilter(onlyUnread: Boolean) { + fun setUnreadFilter(onlyUnread: Boolean) { manga.readFilter = if (onlyUnread) Manga.SHOW_UNREAD else Manga.SHOW_ALL db.updateFlags(manga).executeAsBlocking() refreshChapters() } + /** + * Sets the read filter and requests an UI update. + * + * @param onlyRead whether to display only read chapters or all chapters. + */ + fun setReadFilter(onlyRead: Boolean) { + manga.readFilter = if (onlyRead) Manga.SHOW_READ else Manga.SHOW_ALL + db.updateFlags(manga).executeAsBlocking() + refreshChapters() + } + /** * Sets the download filter and requests an UI update. * @@ -411,6 +425,13 @@ class ChaptersPresenter : BasePresenter() { return manga.readFilter == Manga.SHOW_UNREAD } + /** + * Whether the display only read filter is enabled. + */ + fun onlyRead(): Boolean { + return manga.readFilter == Manga.SHOW_READ + } + /** * Whether the sorting method is descending or ascending. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.kt index 9bc40b823..2f67fd5fb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.kt @@ -1,20 +1,34 @@ package eu.kanade.tachiyomi.ui.manga.info +import android.content.Intent +import android.graphics.Bitmap import android.net.Uri import android.os.Bundle import android.support.customtabs.CustomTabsIntent import android.view.* +import com.afollestad.materialdialogs.MaterialDialog +import com.bumptech.glide.BitmapRequestBuilder +import com.bumptech.glide.BitmapTypeRequest import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.load.resource.bitmap.CenterCrop import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.source.Source import eu.kanade.tachiyomi.data.source.online.OnlineSource import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment +import eu.kanade.tachiyomi.ui.manga.MangaActivity import eu.kanade.tachiyomi.util.getResourceColor import eu.kanade.tachiyomi.util.toast +import jp.wasabeef.glide.transformations.CropCircleTransformation +import jp.wasabeef.glide.transformations.CropSquareTransformation +import jp.wasabeef.glide.transformations.MaskTransformation +import jp.wasabeef.glide.transformations.RoundedCornersTransformation import kotlinx.android.synthetic.main.fragment_manga_info.* import nucleus.factory.RequiresPresenter +import rx.Observable +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers /** * Fragment that shows manga information. @@ -33,6 +47,7 @@ class MangaInfoFragment : BaseRxFragment() { fun newInstance(): MangaInfoFragment { return MangaInfoFragment() } + } override fun onCreate(savedState: Bundle?) { @@ -59,6 +74,8 @@ class MangaInfoFragment : BaseRxFragment() { override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.action_open_in_browser -> openInBrowser() + R.id.action_share -> shareManga() + R.id.action_add_to_home_screen -> addToHomeScreen() else -> return super.onOptionsItemSelected(item) } return true @@ -158,6 +175,95 @@ class MangaInfoFragment : BaseRxFragment() { } } + /** + * Called to run Intent with [Intent.ACTION_SEND], which show share dialog. + */ + private fun shareManga() { + val source = presenter.source as? OnlineSource ?: return + try { + val url = source.mangaDetailsRequest(presenter.manga).url().toString() + val sharingIntent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(android.content.Intent.EXTRA_SUBJECT, presenter.manga.title) + putExtra(android.content.Intent.EXTRA_TEXT, resources.getString(R.string.share_text, presenter.manga.title, url)) + } + startActivity(Intent.createChooser(sharingIntent, resources.getText(R.string.share_subject))) + } catch (e: Exception) { + context.toast(e.message) + } + } + + /** + * Add the manga to the home screen + */ + fun addToHomeScreen() { + val shortcutIntent = activity.intent + shortcutIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + .putExtra(MangaActivity.FROM_LAUNCHER_EXTRA, true) + + val addIntent = Intent() + addIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent) + .action = "com.android.launcher.action.INSTALL_SHORTCUT" + + //Set shortcut title + MaterialDialog.Builder(activity) + .title(R.string.shortcut_title) + .input("", presenter.manga.title, { md, text -> + //Set shortcut title + addIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, text.toString()) + + reshapeIconBitmap(addIntent, + Glide.with(context).load(presenter.manga).asBitmap()) + }) + .negativeText(android.R.string.cancel) + .onNegative { materialDialog, dialogAction -> materialDialog.cancel() } + .show() + } + + fun reshapeIconBitmap(addIntent: Intent, request: BitmapTypeRequest) { + val modes = intArrayOf(R.string.circular_icon, + R.string.rounded_icon, + R.string.square_icon, + R.string.star_icon) + + fun BitmapRequestBuilder.toIcon(): Bitmap { + return this.into(96, 96).get() + } + + MaterialDialog.Builder(activity) + .title(R.string.icon_shape) + .negativeText(android.R.string.cancel) + .items(modes.map { getString(it) }) + .itemsCallback { dialog, view, i, charSequence -> + Observable.fromCallable { + // i = 0: Circular icon + // i = 1: Rounded icon + // i = 2: Square icon + // i = 3: Star icon (because boredom) + when (i) { + 0 -> request.transform(CropCircleTransformation(context)).toIcon() + 1 -> request.transform(RoundedCornersTransformation(context, 5, 0)).toIcon() + 2 -> request.transform(CropSquareTransformation(context)).toIcon() + 3 -> request.transform(CenterCrop(context), MaskTransformation(context, R.drawable.mask_star)).toIcon() + else -> null + } + }.subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ if (it != null) createShortcut(addIntent, it) }, + { context.toast(R.string.icon_creation_fail) }) + }.show() + } + + fun createShortcut(addIntent: Intent, icon: Bitmap) { + //Send shortcut intent + addIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon) + context.sendBroadcast(addIntent) + //Go to launcher to show this shiny new shortcut! + val startMain = Intent(Intent.ACTION_MAIN) + startMain.addCategory(Intent.CATEGORY_HOME).flags = Intent.FLAG_ACTIVITY_NEW_TASK + startActivity(startMain) + } + /** * Update FAB with correct drawable. * diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt index 3c54c6ff8..35941b3d0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt @@ -128,5 +128,4 @@ class MangaInfoPresenter : BasePresenter() { private fun refreshManga() { start(GET_MANGA) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ChapterLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ChapterLoader.kt index 225a48aa5..e7c8a35b9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ChapterLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ChapterLoader.kt @@ -42,9 +42,9 @@ class ChapterLoader( .repeat() .subscribeOn(Schedulers.io()) .subscribe({ - }, { - if (it !is InterruptedException) { - Timber.e(it, it.message) + }, { error -> + if (error !is InterruptedException) { + Timber.e(error) } }) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index bf90a1081..51d30e4ec 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -20,6 +20,7 @@ import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.data.source.model.Page import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.LeftToRightReader @@ -37,10 +38,12 @@ import me.zhanghai.android.systemuihelper.SystemUiHelper import me.zhanghai.android.systemuihelper.SystemUiHelper.* import nucleus.factory.RequiresPresenter import rx.Subscription +import rx.android.schedulers.AndroidSchedulers import rx.subscriptions.CompositeSubscription import timber.log.Timber import uy.kohesive.injekt.injectLazy import java.text.DecimalFormat +import java.util.concurrent.TimeUnit @RequiresPresenter(ReaderPresenter::class) class ReaderActivity : BaseRxActivity() { @@ -69,6 +72,8 @@ class ReaderActivity : BaseRxActivity() { private var customBrightnessSubscription: Subscription? = null + private var customFilterColorSubscription: Subscription? = null + var readerTheme: Int = 0 private set @@ -105,7 +110,7 @@ class ReaderActivity : BaseRxActivity() { setMenuVisibility(menuVisible) - maxBitmapSize = GLUtil.getMaxTextureSize() + maxBitmapSize = Math.min(2048, GLUtil.getMaxTextureSize()) left_chapter.setOnClickListener { if (viewer != null) { @@ -139,6 +144,7 @@ class ReaderActivity : BaseRxActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.action_settings -> ReaderSettingsDialog().show(supportFragmentManager, "settings") + R.id.action_custom_filter -> ReaderCustomFilterDialog().show(supportFragmentManager, "filter") else -> return super.onOptionsItemSelected(item) } return true @@ -149,6 +155,13 @@ class ReaderActivity : BaseRxActivity() { super.onSaveInstanceState(outState) } + override fun onWindowFocusChanged(hasFocus: Boolean) { + super.onWindowFocusChanged(hasFocus) + if (hasFocus) { + setMenuVisibility(menuVisible, animate = false) + } + } + override fun onBackPressed() { val chapterToUpdate = presenter.getMangaSyncChapterToUpdate() @@ -206,7 +219,7 @@ class ReaderActivity : BaseRxActivity() { } fun onChapterError(error: Throwable) { - Timber.e(error, error.message) + Timber.e(error) finish() toast(error.message) } @@ -301,15 +314,18 @@ class ReaderActivity : BaseRxActivity() { return fragment } - fun onPageChanged(currentPageIndex: Int, totalPages: Int) { - val page = currentPageIndex + 1 - page_number.text = "$page/$totalPages" + fun onPageChanged(page: Page) { + presenter.onPageChanged(page) + + val pageNumber = page.pageNumber + 1 + val pageCount = page.chapter.pages!!.size + page_number.text = "$pageNumber/$pageCount" if (page_seekbar.rotation != 180f) { - left_page_text.text = "$page" + left_page_text.text = "$pageNumber" } else { - right_page_text.text = "$page" + right_page_text.text = "$pageNumber" } - page_seekbar.progress = currentPageIndex + page_seekbar.progress = page.pageNumber } fun gotoPageInCurrentChapter(pageIndex: Int) { @@ -319,7 +335,6 @@ class ReaderActivity : BaseRxActivity() { val requestedPage = activePage.chapter.pages!![pageIndex] it.setActivePage(requestedPage) } - } } @@ -344,9 +359,9 @@ class ReaderActivity : BaseRxActivity() { reader_menu_bottom.setOnTouchListener { v, event -> true } page_seekbar.setOnSeekBarChangeListener(object : SimpleSeekBarListener() { - override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) { if (fromUser) { - gotoPageInCurrentChapter(progress) + gotoPageInCurrentChapter(value) } } }) @@ -368,6 +383,9 @@ class ReaderActivity : BaseRxActivity() { subscriptions += preferences.customBrightness().asObservable() .subscribe { setCustomBrightness(it) } + subscriptions += preferences.colorFilter().asObservable() + .subscribe { setColorFilter(it) } + subscriptions += preferences.readerTheme().asObservable() .distinctUntilChanged() .subscribe { applyTheme(it) } @@ -414,6 +432,7 @@ class ReaderActivity : BaseRxActivity() { private fun setCustomBrightness(enabled: Boolean) { if (enabled) { customBrightnessSubscription = preferences.customBrightnessValue().asObservable() + .sample(100, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread()) .subscribe { setCustomBrightnessValue(it) } subscriptions.add(customBrightnessSubscription) @@ -423,6 +442,19 @@ class ReaderActivity : BaseRxActivity() { } } + private fun setColorFilter(enabled: Boolean) { + if (enabled) { + customFilterColorSubscription = preferences.colorFilterValue().asObservable() + .sample(100, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread()) + .subscribe { setColorFilterValue(it) } + + subscriptions.add(customFilterColorSubscription) + } else { + customFilterColorSubscription?.let { subscriptions.remove(it) } + color_overlay.visibility = View.GONE + } + } + /** * Sets the brightness of the screen. Range is [-75, 100]. * From -75 to -1 a semi-transparent black view is shown at the top with the minimum brightness. @@ -449,6 +481,11 @@ class ReaderActivity : BaseRxActivity() { } } + private fun setColorFilterValue(value: Int) { + color_overlay.visibility = View.VISIBLE + color_overlay.setBackgroundColor(value) + } + private fun applyTheme(theme: Int) { readerTheme = theme val rootView = window.decorView.rootView @@ -463,37 +500,42 @@ class ReaderActivity : BaseRxActivity() { } } - private fun setMenuVisibility(visible: Boolean) { + private fun setMenuVisibility(visible: Boolean, animate: Boolean = true) { menuVisible = visible if (visible) { systemUi?.show() reader_menu.visibility = View.VISIBLE - val toolbarAnimation = AnimationUtils.loadAnimation(this, R.anim.enter_from_top) - toolbarAnimation.setAnimationListener(object : SimpleAnimationListener() { - override fun onAnimationStart(animation: Animation) { - // Fix status bar being translucent the first time it's opened. - if (Build.VERSION.SDK_INT >= 21) { - window.addFlags(FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) + if (animate) { + val toolbarAnimation = AnimationUtils.loadAnimation(this, R.anim.enter_from_top) + toolbarAnimation.setAnimationListener(object : SimpleAnimationListener() { + override fun onAnimationStart(animation: Animation) { + // Fix status bar being translucent the first time it's opened. + if (Build.VERSION.SDK_INT >= 21) { + window.addFlags(FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) + } } - } - }) - toolbar.startAnimation(toolbarAnimation) + }) + toolbar.startAnimation(toolbarAnimation) - val bottomMenuAnimation = AnimationUtils.loadAnimation(this, R.anim.enter_from_bottom) - reader_menu_bottom.startAnimation(bottomMenuAnimation) + val bottomMenuAnimation = AnimationUtils.loadAnimation(this, R.anim.enter_from_bottom) + reader_menu_bottom.startAnimation(bottomMenuAnimation) + } } else { systemUi?.hide() - val toolbarAnimation = AnimationUtils.loadAnimation(this, R.anim.exit_to_top) - toolbarAnimation.setAnimationListener(object : SimpleAnimationListener() { - override fun onAnimationEnd(animation: Animation) { - reader_menu.visibility = View.GONE - } - }) - toolbar.startAnimation(toolbarAnimation) - val bottomMenuAnimation = AnimationUtils.loadAnimation(this, R.anim.exit_to_bottom) - reader_menu_bottom.startAnimation(bottomMenuAnimation) + if (animate) { + val toolbarAnimation = AnimationUtils.loadAnimation(this, R.anim.exit_to_top) + toolbarAnimation.setAnimationListener(object : SimpleAnimationListener() { + override fun onAnimationEnd(animation: Animation) { + reader_menu.visibility = View.GONE + } + }) + toolbar.startAnimation(toolbarAnimation) + + val bottomMenuAnimation = AnimationUtils.loadAnimation(this, R.anim.exit_to_bottom) + reader_menu_bottom.startAnimation(bottomMenuAnimation) + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderCustomFilterDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderCustomFilterDialog.kt new file mode 100644 index 000000000..dc820106e --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderCustomFilterDialog.kt @@ -0,0 +1,329 @@ +package eu.kanade.tachiyomi.ui.reader + +import android.app.Dialog +import android.graphics.Color +import android.os.Bundle +import android.support.annotation.ColorInt +import android.support.v4.app.DialogFragment +import android.view.View +import android.widget.SeekBar +import com.afollestad.materialdialogs.MaterialDialog +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.util.plusAssign +import eu.kanade.tachiyomi.widget.SimpleSeekBarListener +import kotlinx.android.synthetic.main.dialog_reader_custom_filter.view.* +import rx.Subscription +import rx.android.schedulers.AndroidSchedulers +import rx.subscriptions.CompositeSubscription +import uy.kohesive.injekt.injectLazy +import java.util.concurrent.TimeUnit + +/** + * Custom dialog which can be used to set overlay value's + */ +class ReaderCustomFilterDialog : DialogFragment() { + + companion object { + /** Integer mask of alpha value **/ + private const val ALPHA_MASK: Long = 0xFF000000 + + /** Integer mask of red value **/ + private const val RED_MASK: Long = 0x00FF0000 + + /** Integer mask of green value **/ + private const val GREEN_MASK: Long = 0x0000FF00 + + /** Integer mask of blue value **/ + private const val BLUE_MASK: Long = 0x000000FF + } + + /** + * Provides operations to manage preferences + */ + private val preferences by injectLazy() + + /** + * Subscription used for filter overlay + */ + private lateinit var subscriptions: CompositeSubscription + + /** + * Subscription used for custom brightness overlay + */ + private var customBrightnessSubscription: Subscription? = null + + /** + * Subscription used for color filter overlay + */ + private var customFilterColorSubscription: Subscription? = null + + /** + * This method will be called after onCreate(Bundle) + * @param savedState The last saved instance state of the Fragment. + */ + override fun onCreateDialog(savedState: Bundle?): Dialog { + val dialog = MaterialDialog.Builder(activity) + .customView(R.layout.dialog_reader_custom_filter, false) + .positiveText(android.R.string.ok) + .build() + + subscriptions = CompositeSubscription() + onViewCreated(dialog.view, savedState) + + return dialog + } + + /** + * Called immediately after onCreateView() + * @param view The View returned by onCreateDialog. + * @param savedInstanceState If non-null, this fragment is being re-constructed + */ + override fun onViewCreated(view: View, savedInstanceState: Bundle?) = with(view) { + // Initialize subscriptions. + subscriptions += preferences.colorFilter().asObservable() + .subscribe { setColorFilter(it, view) } + + subscriptions += preferences.customBrightness().asObservable() + .subscribe { setCustomBrightness(it, view) } + + // Get color and update values + val color = preferences.colorFilterValue().getOrDefault() + val brightness = preferences.customBrightnessValue().getOrDefault() + + val argb = setValues(color, view) + + // Set brightness value + txt_brightness_seekbar_value.text = brightness.toString() + + // Initialize seekBar progress + seekbar_color_filter_alpha.progress = argb[0] + seekbar_color_filter_red.progress = argb[1] + seekbar_color_filter_green.progress = argb[2] + seekbar_color_filter_blue.progress = argb[3] + + // Set listeners + switch_color_filter.isChecked = preferences.colorFilter().getOrDefault() + switch_color_filter.setOnCheckedChangeListener { v, isChecked -> + preferences.colorFilter().set(isChecked) + } + + custom_brightness.isChecked = preferences.customBrightness().getOrDefault() + custom_brightness.setOnCheckedChangeListener { v, isChecked -> + preferences.customBrightness().set(isChecked) + } + + seekbar_color_filter_alpha.setOnSeekBarChangeListener(object : SimpleSeekBarListener() { + override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) { + if (fromUser) { + setColorValue(value, ALPHA_MASK, 24) + } + } + }) + + seekbar_color_filter_red.setOnSeekBarChangeListener(object : SimpleSeekBarListener() { + override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) { + if (fromUser) { + setColorValue(value, RED_MASK, 16) + } + } + }) + + seekbar_color_filter_green.setOnSeekBarChangeListener(object : SimpleSeekBarListener() { + override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) { + if (fromUser) { + setColorValue(value, GREEN_MASK, 8) + } + } + }) + + seekbar_color_filter_blue.setOnSeekBarChangeListener(object : SimpleSeekBarListener() { + override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) { + if (fromUser) { + setColorValue(value, BLUE_MASK, 0) + } + } + }) + brightness_seekbar.progress = preferences.customBrightnessValue().getOrDefault() + brightness_seekbar.setOnSeekBarChangeListener(object : SimpleSeekBarListener() { + override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) { + if (fromUser) { + preferences.customBrightnessValue().set(value) + } + } + }) + + } + + /** + * Set enabled status of seekBars belonging to color filter + * @param enabled determines if seekBar gets enabled + * @param view view of the dialog + */ + private fun setColorFilterSeekBar(enabled: Boolean, view: View) = with(view) { + seekbar_color_filter_red.isEnabled = enabled + seekbar_color_filter_green.isEnabled = enabled + seekbar_color_filter_blue.isEnabled = enabled + seekbar_color_filter_alpha.isEnabled = enabled + } + + /** + * Set enabled status of seekBars belonging to custom brightness + * @param enabled value which determines if seekBar gets enabled + * @param view view of the dialog + */ + private fun setCustomBrightnessSeekBar(enabled: Boolean, view: View) = with(view) { + brightness_seekbar.isEnabled = enabled + } + + /** + * Set the text value's of color filter + * @param color integer containing color information + * @param view view of the dialog + */ + fun setValues(color: Int, view: View): Array { + val alpha = getAlphaFromColor(color) + val red = getRedFromColor(color) + val green = getGreenFromColor(color) + val blue = getBlueFromColor(color) + + //Initialize values + with(view) { + txt_color_filter_alpha_value.text = alpha.toString() + + txt_color_filter_red_value.text = red.toString() + + txt_color_filter_green_value.text = green.toString() + + txt_color_filter_blue_value.text = blue.toString() + } + return arrayOf(alpha, red, green, blue) + } + + /** + * Manages the custom brightness value subscription + * @param enabled determines if the subscription get (un)subscribed + * @param view view of the dialog + */ + private fun setCustomBrightness(enabled: Boolean, view: View) { + if (enabled) { + customBrightnessSubscription = preferences.customBrightnessValue().asObservable() + .sample(100, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread()) + .subscribe { setCustomBrightnessValue(it, view) } + + subscriptions.add(customBrightnessSubscription) + } else { + customBrightnessSubscription?.let { subscriptions.remove(it) } + setCustomBrightnessValue(0, view, true) + } + setCustomBrightnessSeekBar(enabled, view) + } + + /** + * Sets the brightness of the screen. Range is [-75, 100]. + * From -75 to -1 a semi-transparent black view is shown at the top with the minimum brightness. + * From 1 to 100 it sets that value as brightness. + * 0 sets system brightness and hides the overlay. + */ + private fun setCustomBrightnessValue(value: Int, view: View, isDisabled: Boolean = false) = with(view) { + // Set black overlay visibility. + if (value < 0) { + brightness_overlay.visibility = View.VISIBLE + val alpha = (Math.abs(value) * 2.56).toInt() + brightness_overlay.setBackgroundColor(Color.argb(alpha, 0, 0, 0)) + } else { + brightness_overlay.visibility = View.GONE + } + + if (!isDisabled) + txt_brightness_seekbar_value.text = value.toString() + } + + /** + * Manages the color filter value subscription + * @param enabled determines if the subscription get (un)subscribed + * @param view view of the dialog + */ + private fun setColorFilter(enabled: Boolean, view: View) { + if (enabled) { + customFilterColorSubscription = preferences.colorFilterValue().asObservable() + .sample(100, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread()) + .subscribe { setColorFilterValue(it, view) } + + subscriptions.add(customFilterColorSubscription) + } else { + customFilterColorSubscription?.let { subscriptions.remove(it) } + view.color_overlay.visibility = View.GONE + } + setColorFilterSeekBar(enabled, view) + } + + /** + * Sets the color filter overlay of the screen. Determined by HEX of integer + * @param color hex of color. + * @param view view of the dialog + */ + private fun setColorFilterValue(@ColorInt color: Int, view: View) = with(view) { + color_overlay.visibility = View.VISIBLE + color_overlay.setBackgroundColor(color) + setValues(color, view) + } + + /** + * Updates the color value in preference + * @param color value of color range [0,255] + * @param mask contains hex mask of chosen color + * @param bitShift amounts of bits that gets shifted to receive value + */ + fun setColorValue(color: Int, mask: Long, bitShift: Int) { + val currentColor = preferences.colorFilterValue().getOrDefault() + val updatedColor = (color shl bitShift) or (currentColor and mask.inv().toInt()) + preferences.colorFilterValue().set(updatedColor) + } + + /** + * Returns the alpha value from the Color Hex + * @param color color hex as int + * @return alpha of color + */ + fun getAlphaFromColor(color: Int): Int { + return color shr 24 and 0xFF + } + + /** + * Returns the red value from the Color Hex + * @param color color hex as int + * @return red of color + */ + fun getRedFromColor(color: Int): Int { + return color shr 16 and 0xFF + } + + /** + * Returns the green value from the Color Hex + * @param color color hex as int + * @return green of color + */ + fun getGreenFromColor(color: Int): Int { + return color shr 8 and 0xFF + } + + /** + * Returns the blue value from the Color Hex + * @param color color hex as int + * @return blue of color + */ + fun getBlueFromColor(color: Int): Int { + return color and 0xFF + } + + /** + * Called when dialog is dismissed + */ + override fun onDestroyView() { + subscriptions.unsubscribe() + super.onDestroyView() + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt index bdd3eb0fa..c349779d6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt @@ -21,6 +21,7 @@ import rx.Observable import rx.Subscription import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers +import timber.log.Timber import uy.kohesive.injekt.injectLazy import java.io.File import java.util.* @@ -228,12 +229,14 @@ class ReaderPresenter : BasePresenter() { * strategy set for the manga. * * @param chapter the current active chapter. + * @param previousChapterAmount the desired number of chapters preceding the current active chapter (Default: 1). + * @param nextChapterAmount the desired number of chapters succeeding the current active chapter (Default: 1). */ - private fun getAdjacentChaptersStrategy(chapter: ReaderChapter) = when (manga.sorting) { + private fun getAdjacentChaptersStrategy(chapter: ReaderChapter, previousChapterAmount: Int = 1, nextChapterAmount: Int = 1) = when (manga.sorting) { Manga.SORTING_SOURCE -> { val currChapterIndex = chapterList.indexOfFirst { chapter.id == it.id } - val nextChapter = chapterList.getOrNull(currChapterIndex + 1) - val prevChapter = chapterList.getOrNull(currChapterIndex - 1) + val nextChapter = chapterList.getOrNull(currChapterIndex + nextChapterAmount) + val prevChapter = chapterList.getOrNull(currChapterIndex - previousChapterAmount) Pair(prevChapter, nextChapter) } Manga.SORTING_NUMBER -> { @@ -241,18 +244,18 @@ class ReaderPresenter : BasePresenter() { val chapterNumber = chapter.chapter_number var prevChapter: ReaderChapter? = null - for (i in (currChapterIndex - 1) downTo 0) { + for (i in (currChapterIndex - previousChapterAmount) downTo 0) { val c = chapterList[i] - if (c.chapter_number < chapterNumber && c.chapter_number >= chapterNumber - 1) { + if (c.chapter_number < chapterNumber && c.chapter_number >= chapterNumber - previousChapterAmount) { prevChapter = c break } } var nextChapter: ReaderChapter? = null - for (i in (currChapterIndex + 1) until chapterList.size) { + for (i in (currChapterIndex + nextChapterAmount) until chapterList.size) { val c = chapterList[i] - if (c.chapter_number > chapterNumber && c.chapter_number <= chapterNumber + 1) { + if (c.chapter_number > chapterNumber && c.chapter_number <= chapterNumber + nextChapterAmount) { nextChapter = c break } @@ -344,42 +347,45 @@ class ReaderPresenter : BasePresenter() { fun onChapterLeft() { // Reference these locally because they are needed later from another thread. val chapter = chapter - val prevChapter = prevChapter val pages = chapter.pages ?: return - Observable - .fromCallable { - // Chapters with 1 page don't trigger page changes, so mark them as read. - if (pages.size == 1) { - chapter.read = true - } + Observable.fromCallable { + // Chapters with 1 page don't trigger page changes, so mark them as read. + if (pages.size == 1) { + chapter.read = true + } - if (!chapter.isDownloaded) { - source.let { if (it is OnlineSource) it.savePageList(chapter, pages) } - } + // Cache current page list progress for online chapters to allow a faster reopen + if (!chapter.isDownloaded) { + source.let { if (it is OnlineSource) it.savePageList(chapter, pages) } + } - // Cache current page list progress for online chapters to allow a faster reopen - if (chapter.read) { - // Check if remove after read is selected by user - if (prefs.removeAfterRead()) { - if (prefs.removeAfterReadPrevious() ) { - if (prevChapter != null) { - deleteChapter(prevChapter, manga) - } - } else { - deleteChapter(chapter, manga) - } - } - } - - db.updateChapterProgress(chapter).executeAsBlocking() - - val history = History.create(chapter).apply { last_read = Date().time } - db.updateHistoryLastRead(history).executeAsBlocking() + if (chapter.read) { + val removeAfterReadSlots = prefs.removeAfterReadSlots() + when (removeAfterReadSlots) { + // Setting disabled + -1 -> { /**Empty function**/ } + // Remove current read chapter + 0 -> deleteChapter(chapter, manga) + // Remove previous chapter specified by user in settings. + else -> getAdjacentChaptersStrategy(chapter, removeAfterReadSlots) + .first?.let { deleteChapter(it, manga) } } - .subscribeOn(Schedulers.io()) - .subscribe() + } + + db.updateChapterProgress(chapter).executeAsBlocking() + + try { + val history = History.create(chapter).apply { last_read = Date().time } + db.updateHistoryLastRead(history).executeAsBlocking() + } catch (error: Exception) { + // TODO find out why it crashes + Timber.e(error) + } + } + .subscribeOn(Schedulers.io()) + .subscribe() } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSettingsDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSettingsDialog.kt index 75f78806b..56659e637 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSettingsDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSettingsDialog.kt @@ -11,7 +11,6 @@ import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.util.plusAssign import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener import kotlinx.android.synthetic.main.dialog_reader_settings.view.* -import org.adw.library.widgets.discreteseekbar.DiscreteSeekBar import rx.Observable import rx.android.schedulers.AndroidSchedulers import rx.subscriptions.CompositeSubscription @@ -84,24 +83,6 @@ class ReaderSettingsDialog : DialogFragment() { fullscreen.setOnCheckedChangeListener { v, isChecked -> preferences.fullscreen().set(isChecked) } - - custom_brightness.isChecked = preferences.customBrightness().getOrDefault() - custom_brightness.setOnCheckedChangeListener { v, isChecked -> - preferences.customBrightness().set(isChecked) - } - - brightness_seekbar.progress = preferences.customBrightnessValue().getOrDefault() - brightness_seekbar.setOnProgressChangeListener(object : DiscreteSeekBar.OnProgressChangeListener { - override fun onProgressChanged(seekBar: DiscreteSeekBar, value: Int, fromUser: Boolean) { - if (fromUser) { - preferences.customBrightnessValue().set(value) - } - } - - override fun onStartTrackingTouch(seekBar: DiscreteSeekBar) {} - - override fun onStopTrackingTouch(seekBar: DiscreteSeekBar) {} - }) } override fun onDestroyView() { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/base/BaseReader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/base/BaseReader.kt index 360696b62..1e6667fee 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/base/BaseReader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/base/BaseReader.kt @@ -66,16 +66,6 @@ abstract class BaseReader : BaseFragment() { */ private var hasRequestedNextChapter: Boolean = false - /** - * Updates the reader activity with the active page. - */ - fun updatePageNumber() { - val activePage = getActivePage() - if (activePage != null) { - readerActivity.onPageChanged(activePage.pageNumber, activePage.chapter.pages!!.size) - } - } - /** * Returns the active page. */ @@ -91,11 +81,13 @@ abstract class BaseReader : BaseFragment() { fun onPageChanged(position: Int) { val oldPage = pages[currentPage] val newPage = pages[position] - readerActivity.presenter.onPageChanged(newPage) val oldChapter = oldPage.chapter val newChapter = newPage.chapter + // Update page indicator and seekbar + readerActivity.onPageChanged(newPage) + // Active chapter has changed. if (oldChapter.id != newChapter.id) { readerActivity.onEnterChapter(newPage.chapter, newPage.pageNumber) @@ -108,7 +100,6 @@ abstract class BaseReader : BaseFragment() { } currentPage = position - updatePageNumber() } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReaderFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PageView.kt similarity index 54% rename from app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReaderFragment.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PageView.kt index 0c172a086..c3d9bd7c7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReaderFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PageView.kt @@ -1,23 +1,24 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager +import android.content.Context import android.graphics.PointF -import android.os.Bundle -import android.support.v4.content.ContextCompat -import android.view.LayoutInflater +import android.util.AttributeSet import android.view.MotionEvent import android.view.View -import android.view.ViewGroup +import android.widget.FrameLayout import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.source.model.Page -import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.viewer.base.PageDecodeErrorLayout +import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerReader.Companion.ALIGN_CENTER +import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerReader.Companion.ALIGN_LEFT +import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerReader.Companion.ALIGN_RIGHT import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.RightToLeftReader import eu.kanade.tachiyomi.ui.reader.viewer.pager.vertical.VerticalReader -import kotlinx.android.synthetic.main.chapter_image.* -import kotlinx.android.synthetic.main.item_pager_reader.* +import kotlinx.android.synthetic.main.chapter_image.view.* +import kotlinx.android.synthetic.main.item_pager_reader.view.* import rx.Observable import rx.Subscription import rx.android.schedulers.AndroidSchedulers @@ -25,41 +26,15 @@ import rx.subjects.PublishSubject import rx.subjects.SerializedSubject import java.io.File import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicInteger -/** - * Fragment for a single page of the ViewPager reader. - * All the elements from the layout file "item_pager_reader" are available in this class. - */ -class PagerReaderFragment : BaseFragment() { - - companion object { - /** - * Creates a new instance of this fragment. - * - * @return a new instance of [PagerReaderFragment]. - */ - fun newInstance(): PagerReaderFragment { - return PagerReaderFragment() - } - } +class PageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) +: FrameLayout(context, attrs) { /** * Page of a chapter. */ var page: Page? = null - set(value) { - field = value - // Observe status if the view is initialized - if (view != null) { - observeStatus() - } - } - - /** - * Position of the fragment in the adapter. - */ - var position = -1 + private set /** * Subscription for progress changes of the page. @@ -71,47 +46,35 @@ class PagerReaderFragment : BaseFragment() { */ private var statusSubscription: Subscription? = null - /** - * Text color for black theme. - */ - private val whiteColor by lazy { ContextCompat.getColor(context, R.color.textColorSecondaryDark) } + fun initialize(reader: PagerReader, page: Page?) { + val activity = reader.activity as ReaderActivity - /** - * Text color for white theme. - */ - private val blackColor by lazy { ContextCompat.getColor(context, R.color.textColorSecondaryLight) } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? { - return inflater.inflate(R.layout.item_pager_reader, container, false) - } - - override fun onViewCreated(view: View, savedState: Bundle?) { - if (readerActivity.readerTheme == ReaderActivity.BLACK_THEME) { - progress_text.setTextColor(whiteColor) - } else { - progress_text.setTextColor(blackColor) + when (activity.readerTheme) { + ReaderActivity.BLACK_THEME -> progress_text.setTextColor(reader.whiteColor) + ReaderActivity.WHITE_THEME -> progress_text.setTextColor(reader.blackColor) } - if (pagerReader is RightToLeftReader) { - view.rotation = -180f + if (reader is RightToLeftReader) { + rotation = -180f } with(image_view) { - setMaxBitmapDimensions(readerActivity.maxBitmapSize) + setMaxBitmapDimensions((reader.activity as ReaderActivity).maxBitmapSize) setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED) setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE) - setMinimumScaleType(pagerReader.scaleType) - setMinimumDpi(50) - setRegionDecoderClass(pagerReader.regionDecoderClass) - setBitmapDecoderClass(pagerReader.bitmapDecoderClass) - setVerticalScrollingParent(pagerReader is VerticalReader) - setOnTouchListener { v, motionEvent -> pagerReader.gestureDetector.onTouchEvent(motionEvent) } + setMinimumScaleType(reader.scaleType) + setMinimumDpi(90) + setMinimumTileDpi(180) + setRegionDecoderClass(reader.regionDecoderClass) + setBitmapDecoderClass(reader.bitmapDecoderClass) + setVerticalScrollingParent(reader is VerticalReader) + setOnTouchListener { v, motionEvent -> reader.gestureDetector.onTouchEvent(motionEvent) } setOnImageEventListener(object : SubsamplingScaleImageView.DefaultOnImageEventListener() { override fun onReady() { - when (pagerReader.zoomType) { - PagerReader.ALIGN_LEFT -> setScaleAndCenter(scale, PointF(0f, 0f)) - PagerReader.ALIGN_RIGHT -> setScaleAndCenter(scale, PointF(sWidth.toFloat(), 0f)) - PagerReader.ALIGN_CENTER -> { + when (reader.zoomType) { + ALIGN_LEFT -> setScaleAndCenter(scale, PointF(0f, 0f)) + ALIGN_RIGHT -> setScaleAndCenter(scale, PointF(sWidth.toFloat(), 0f)) + ALIGN_CENTER -> { val newCenter = center newCenter.y = 0f setScaleAndCenter(scale, newCenter) @@ -120,27 +83,34 @@ class PagerReaderFragment : BaseFragment() { } override fun onImageLoadError(e: Exception) { - onImageDecodeError() + onImageDecodeError(activity) } }) } retry_button.setOnTouchListener { v, event -> if (event.action == MotionEvent.ACTION_UP) { - readerActivity.presenter.retryPage(page) + activity.presenter.retryPage(page) } true } - observeStatus() + if (page != null) { + this.page = page + observeStatus() + } } - override fun onDestroyView() { + fun cleanup() { unsubscribeProgress() unsubscribeStatus() image_view.setOnTouchListener(null) image_view.setOnImageEventListener(null) - super.onDestroyView() + } + + override fun onDetachedFromWindow() { + cleanup() + super.onDetachedFromWindow() } /** @@ -149,33 +119,31 @@ class PagerReaderFragment : BaseFragment() { * @see processStatus */ private fun observeStatus() { - page?.let { page -> - val statusSubject = SerializedSubject(PublishSubject.create()) - page.setStatusSubject(statusSubject) + statusSubscription?.unsubscribe() + val page = page ?: return - statusSubscription?.unsubscribe() - statusSubscription = statusSubject.startWith(page.status) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { processStatus(it) } - } + val statusSubject = SerializedSubject(PublishSubject.create()) + page.setStatusSubject(statusSubject) + + statusSubscription = statusSubject.startWith(page.status) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { processStatus(it) } } /** * Observes the progress of the page and updates view. */ private fun observeProgress() { - val currentValue = AtomicInteger(-1) - progressSubscription?.unsubscribe() + val page = page ?: return + progressSubscription = Observable.interval(100, TimeUnit.MILLISECONDS) + .map { page.progress } + .distinctUntilChanged() .onBackpressureLatest() .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - // Refresh UI only if progress change - if (page?.progress != currentValue.get()) { - currentValue.set(page?.progress ?: 0) - progress_text.text = getString(R.string.download_progress, currentValue.get()) - } + .subscribe { progress -> + progress_text.text = context.getString(R.string.download_progress, progress) } } @@ -269,27 +237,13 @@ class PagerReaderFragment : BaseFragment() { /** * Called when an image fails to decode. */ - private fun onImageDecodeError() { - val view = view as? ViewGroup ?: return - + private fun onImageDecodeError(activity: ReaderActivity) { page?.let { page -> - val errorLayout = PageDecodeErrorLayout(context, page, readerActivity.readerTheme, - { readerActivity.presenter.retryPage(page) }) + val errorLayout = PageDecodeErrorLayout(context, page, activity.readerTheme, + { activity.presenter.retryPage(page) }) - view.addView(errorLayout) + addView(errorLayout) } } - /** - * Property to get the reader activity. - */ - private val readerActivity: ReaderActivity - get() = activity as ReaderActivity - - /** - * Property to get the pager reader. - */ - private val pagerReader: PagerReader - get() = parentFragment as PagerReader - -} +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReader.kt index f1c0e32a5..6d9a4bb63 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReader.kt @@ -1,5 +1,6 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager +import android.support.v4.content.ContextCompat import android.view.GestureDetector import android.view.MotionEvent import android.view.ViewGroup @@ -90,13 +91,23 @@ abstract class PagerReader : BaseReader() { var zoomType = 1 private set + /** + * Text color for black theme. + */ + val whiteColor by lazy { ContextCompat.getColor(context, R.color.textColorSecondaryDark) } + + /** + * Text color for white theme. + */ + val blackColor by lazy { ContextCompat.getColor(context, R.color.textColorSecondaryLight) } + /** * Initializes the pager. * * @param pager the pager to initialize. */ protected fun initializePager(pager: Pager) { - adapter = PagerReaderAdapter(childFragmentManager) + adapter = PagerReaderAdapter(this) this.pager = pager.apply { setLayoutParams(ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)) @@ -161,14 +172,16 @@ abstract class PagerReader : BaseReader() { protected fun createGestureDetector(): GestureDetector { return GestureDetector(activity, object : GestureDetector.SimpleOnGestureListener() { override fun onSingleTapConfirmed(e: MotionEvent): Boolean { - val positionX = e.x + if (isAdded) { + val positionX = e.x - if (positionX < pager.width * LEFT_REGION) { - if (tappingEnabled) onLeftSideTap() - } else if (positionX > pager.width * RIGHT_REGION) { - if (tappingEnabled) onRightSideTap() - } else { - readerActivity.toggleMenu() + if (positionX < pager.width * LEFT_REGION) { + if (tappingEnabled) onLeftSideTap() + } else if (positionX > pager.width * RIGHT_REGION) { + if (tappingEnabled) onRightSideTap() + } else { + readerActivity.toggleMenu() + } } return true } @@ -208,8 +221,11 @@ abstract class PagerReader : BaseReader() { protected fun setPagesOnAdapter() { if (pages.isNotEmpty()) { adapter.pages = pages - setActivePage(currentPage) - updatePageNumber() + if (currentPage == pager.currentItem) { + onPageChanged(currentPage) + } else { + setActivePage(currentPage) + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReaderAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReaderAdapter.kt index 9c2990b51..fe3fadfc0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReaderAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReaderAdapter.kt @@ -1,19 +1,16 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager -import android.support.v4.app.Fragment -import android.support.v4.app.FragmentManager -import android.support.v4.app.FragmentStatePagerAdapter -import android.support.v4.view.PagerAdapter +import android.view.View import android.view.ViewGroup - +import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.source.model.Page +import eu.kanade.tachiyomi.util.inflate +import eu.kanade.tachiyomi.widget.ViewPagerAdapter /** * Adapter of pages for a ViewPager. - * - * @param fm the fragment manager. */ -class PagerReaderAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm) { +class PagerReaderAdapter(private val reader: PagerReader) : ViewPagerAdapter() { /** * Pages stored in the adapter. @@ -24,6 +21,12 @@ class PagerReaderAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm) { notifyDataSetChanged() } + override fun createView(container: ViewGroup, position: Int): View { + val view = container.inflate(R.layout.item_pager_reader) as PageView + view.initialize(reader, pages?.getOrNull(position)) + return view + } + /** * Returns the number of pages. * @@ -33,46 +36,4 @@ class PagerReaderAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm) { return pages?.size ?: 0 } - /** - * Creates a new fragment for the given position when it's called. - * - * @param position the position to instantiate. - * @return a fragment for the given position. - */ - override fun getItem(position: Int): Fragment { - return PagerReaderFragment.newInstance() - } - - /** - * Instantiates a fragment in the given position. - * - * @param container the parent view. - * @param position the position to instantiate. - * @return an instance of a fragment for the given position. - */ - override fun instantiateItem(container: ViewGroup, position: Int): Any { - val f = super.instantiateItem(container, position) as PagerReaderFragment - f.page = pages!![position] - f.position = position - return f - } - - /** - * Returns the position of a given item. - * - * @param obj the item to find its position. - * @return the position for the item. - */ - override fun getItemPosition(obj: Any): Int { - val f = obj as PagerReaderFragment - val position = f.position - if (position >= 0 && position < count) { - if (pages!![position] === f.page) { - return PagerAdapter.POSITION_UNCHANGED - } else { - return PagerAdapter.POSITION_NONE - } - } - return super.getItemPosition(obj) - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonHolder.kt index ff077882e..6e558a4df 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonHolder.kt @@ -49,7 +49,8 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter) setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED) setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE) setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_FIT_WIDTH) - maxScale = 10f + setMinimumDpi(90) + setMinimumTileDpi(180) setRegionDecoderClass(webtoonReader.regionDecoderClass) setBitmapDecoderClass(webtoonReader.bitmapDecoderClass) setVerticalScrollingParent(true) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonReader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonReader.kt index 7565f8d5e..f08eecd55 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonReader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonReader.kt @@ -85,9 +85,9 @@ class WebtoonReader : BaseReader() { recycler.adapter = adapter recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) { - val page = layoutManager.findLastVisibleItemPosition() - if (page != currentPage) { - onPageChanged(page) + val index = layoutManager.findLastVisibleItemPosition() + if (index != currentPage) { + pages.getOrNull(index)?.let { onPageChanged(index) } } } }) @@ -127,14 +127,16 @@ class WebtoonReader : BaseReader() { protected fun createGestureDetector(): GestureDetector { return GestureDetector(context, object : SimpleOnGestureListener() { override fun onSingleTapConfirmed(e: MotionEvent): Boolean { - val positionX = e.x + if (isAdded) { + val positionX = e.x - if (positionX < recycler.width * LEFT_REGION) { - if (tappingEnabled) moveToPrevious() - } else if (positionX > recycler.width * RIGHT_REGION) { - if (tappingEnabled) moveToNext() - } else { - readerActivity.toggleMenu() + if (positionX < recycler.width * LEFT_REGION) { + if (tappingEnabled) moveToPrevious() + } else if (positionX > recycler.width * RIGHT_REGION) { + if (tappingEnabled) moveToNext() + } else { + readerActivity.toggleMenu() + } } return true } @@ -148,8 +150,7 @@ class WebtoonReader : BaseReader() { * @param currentPage the initial page to display. */ override fun onChapterSet(chapter: ReaderChapter, currentPage: Page) { - // Restoring current page is not supported. It's getting weird scrolling jumps - // this.currentPage = currentPage; + this.currentPage = currentPage.pageNumber // Make sure the view is already initialized. if (view != null) { @@ -177,7 +178,7 @@ class WebtoonReader : BaseReader() { if (pages.isNotEmpty()) { adapter.pages = pages recycler.adapter = adapter - updatePageNumber() + onPageChanged(currentPage) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersFragment.kt index 8734456f9..c59cd7f71 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersFragment.kt @@ -253,7 +253,7 @@ class RecentChaptersFragment */ fun onChaptersDeletedError(error: Throwable) { dismissDeletingDialog() - Timber.e(error, error.message) + Timber.e(error) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersPresenter.kt index f08af4599..219b6b807 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersPresenter.kt @@ -71,7 +71,7 @@ class RecentChaptersPresenter : BasePresenter() { // Set chapter status view.onChapterStatusChange(download) }, - { view, error -> Timber.e(error.cause, error.message) } + { view, error -> Timber.e(error) } ) if (savedState == null) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadPresenter.kt index bcae72c1a..9f50473a4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadPresenter.kt @@ -115,7 +115,7 @@ class RecentlyReadPresenter : BasePresenter() { .subscribeFirst({ view, chapter -> view.onOpenNextChapter(chapter, manga) }, { view, error -> - Timber.e(error, error.message) + Timber.e(error) }) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutFragment.kt index 46764bfeb..67fcf8865 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutFragment.kt @@ -1,37 +1,27 @@ package eu.kanade.tachiyomi.ui.setting import android.os.Bundle -import android.support.v7.preference.SwitchPreferenceCompat import android.support.v7.preference.XpPreferenceFragment import android.view.View import com.afollestad.materialdialogs.MaterialDialog import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.updater.GithubUpdateChecker -import eu.kanade.tachiyomi.data.updater.UpdateDownloader +import eu.kanade.tachiyomi.data.updater.GithubUpdateResult +import eu.kanade.tachiyomi.data.updater.UpdateCheckerService +import eu.kanade.tachiyomi.data.updater.UpdateDownloaderService import eu.kanade.tachiyomi.util.toast +import net.xpece.android.support.preference.SwitchPreference import rx.Subscription import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers +import timber.log.Timber import java.text.DateFormat import java.text.ParseException import java.text.SimpleDateFormat import java.util.* class SettingsAboutFragment : SettingsFragment() { - /** - * Checks for new releases - */ - private val updateChecker by lazy { GithubUpdateChecker(activity) } - - /** - * The subscribtion service of the obtained release object - */ - private var releaseSubscription: Subscription? = null - - val automaticUpdateToggle by lazy { - findPreference(getString(R.string.pref_enable_automatic_updates_key)) as SwitchPreferenceCompat - } companion object { fun newInstance(rootKey: String): SettingsAboutFragment { @@ -41,6 +31,18 @@ class SettingsAboutFragment : SettingsFragment() { } } + /** + * Checks for new releases + */ + private val updateChecker by lazy { GithubUpdateChecker() } + + /** + * The subscribtion service of the obtained release object + */ + private var releaseSubscription: Subscription? = null + + val automaticUpdates: SwitchPreference by bindPref(R.string.pref_enable_automatic_updates_key) + override fun onViewCreated(view: View, savedState: Bundle?) { super.onViewCreated(view, savedState) @@ -48,15 +50,30 @@ class SettingsAboutFragment : SettingsFragment() { val buildTime = findPreference(getString(R.string.pref_build_time)) findPreference("acra.enable").isEnabled = false; - version.summary = BuildConfig.VERSION_NAME + version.summary = if (BuildConfig.DEBUG) + "r" + BuildConfig.COMMIT_COUNT + else + BuildConfig.VERSION_NAME - //TODO One glorious day enable this and add the magnificent option for auto update checking. - // automaticUpdateToggle.isEnabled = true - // automaticUpdateToggle.setOnPreferenceChangeListener { preference, any -> - // val status = any as Boolean - // UpdateDownloaderAlarm.startAlarm(activity, 12, status) - // true - // } + if (!BuildConfig.DEBUG && BuildConfig.INCLUDE_UPDATER) { + //Set onClickListener to check for new version + version.setOnPreferenceClickListener { + checkVersion() + true + } + + automaticUpdates.setOnPreferenceChangeListener { preference, any -> + val checked = any as Boolean + if (checked) { + UpdateCheckerService.setupTask(context) + } else { + UpdateCheckerService.cancelTask(context) + } + true + } + } else { + automaticUpdates.isVisible = false + } buildTime.summary = getFormattedBuildTime() } @@ -88,36 +105,35 @@ class SettingsAboutFragment : SettingsFragment() { private fun checkVersion() { releaseSubscription?.unsubscribe() - releaseSubscription = updateChecker.checkForApplicationUpdate() + context.toast(R.string.update_check_look_for_updates) + + releaseSubscription = updateChecker.checkForUpdate() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ release -> - //Get version of latest release - var newVersion = release.version - newVersion = newVersion.replace("[^\\d.]".toRegex(), "") + .subscribe({ result -> + when (result) { + is GithubUpdateResult.NewUpdate -> { + val body = result.release.changeLog + val url = result.release.downloadLink - //Check if latest version is different from current version - if (newVersion != BuildConfig.VERSION_NAME) { - val downloadLink = release.downloadLink - val body = release.changeLog - - //Create confirmation window - MaterialDialog.Builder(activity) - .title(R.string.update_check_title) - .content(body) - .positiveText(getString(R.string.update_check_confirm)) - .negativeText(getString(R.string.update_check_ignore)) - .onPositive { dialog, which -> - // User output that download has started - activity.toast(R.string.update_check_download_started) - // Start download - UpdateDownloader(activity.applicationContext).execute(downloadLink) - }.show() - } else { - activity.toast(R.string.update_check_no_new_updates) + // Create confirmation window + MaterialDialog.Builder(context) + .title(R.string.update_check_title) + .content(body) + .positiveText(getString(R.string.update_check_confirm)) + .negativeText(getString(R.string.update_check_ignore)) + .onPositive { dialog, which -> + // Start download + UpdateDownloaderService.downloadUpdate(context, url) + } + .show() + } + is GithubUpdateResult.NoNewUpdate -> { + context.toast(R.string.update_check_no_new_updates) + } } - }, { - it.printStackTrace() + }, { error -> + Timber.e(error) }) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedFragment.kt index d4585d837..ad3c918fa 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedFragment.kt @@ -1,12 +1,14 @@ package eu.kanade.tachiyomi.ui.setting import android.os.Bundle +import android.support.v7.preference.Preference import android.support.v7.preference.XpPreferenceFragment import android.view.View import com.afollestad.materialdialogs.MaterialDialog import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.library.LibraryUpdateService import eu.kanade.tachiyomi.data.network.NetworkHelper import eu.kanade.tachiyomi.util.plusAssign import eu.kanade.tachiyomi.util.toast @@ -32,11 +34,13 @@ class SettingsAdvancedFragment : SettingsFragment() { private val db: DatabaseHelper by injectLazy() - private val clearCache by lazy { findPreference(getString(R.string.pref_clear_chapter_cache_key)) } + private val clearCache: Preference by bindPref(R.string.pref_clear_chapter_cache_key) - private val clearDatabase by lazy { findPreference(getString(R.string.pref_clear_database_key)) } + private val clearDatabase: Preference by bindPref(R.string.pref_clear_database_key) - private val clearCookies by lazy { findPreference(getString(R.string.pref_clear_cookies_key)) } + private val clearCookies: Preference by bindPref(R.string.pref_clear_cookies_key) + + private val refreshMetadata: Preference by bindPref(R.string.pref_refresh_library_metadata_key) override fun onViewCreated(view: View, savedState: Bundle?) { super.onViewCreated(view, savedState) @@ -57,6 +61,11 @@ class SettingsAdvancedFragment : SettingsFragment() { clearDatabase() true } + + refreshMetadata.setOnPreferenceClickListener { + LibraryUpdateService.start(context, details = true) + true + } } private fun clearChapterCache() { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadsFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadsFragment.kt index 9ff03c7f6..3168cb1ba 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadsFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadsFragment.kt @@ -5,6 +5,7 @@ import android.content.Intent import android.os.Bundle import android.os.Environment import android.support.v4.content.ContextCompat +import android.support.v7.preference.Preference import android.support.v7.preference.XpPreferenceFragment import android.support.v7.widget.RecyclerView import android.view.View @@ -36,7 +37,7 @@ class SettingsDownloadsFragment : SettingsFragment() { private val preferences: PreferencesHelper by injectLazy() - val downloadDirPref by lazy { findPreference(getString(R.string.pref_download_directory_key)) } + val downloadDirPref: Preference by bindPref(R.string.pref_download_directory_key) override fun onViewCreated(view: View, savedState: Bundle?) { super.onViewCreated(view, savedState) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsFragment.kt index 89e13455c..882b24fa2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsFragment.kt @@ -4,12 +4,13 @@ import android.os.Bundle import android.support.annotation.CallSuper import android.support.graphics.drawable.VectorDrawableCompat import android.support.v4.content.ContextCompat +import android.support.v7.preference.Preference import android.support.v7.preference.XpPreferenceFragment import android.view.View import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.util.getResourceId import net.xpece.android.support.preference.PreferenceIconHelper import net.xpece.android.support.preference.PreferenceScreenNavigationStrategy -import net.xpece.android.support.preference.Util import rx.subscriptions.CompositeSubscription open class SettingsFragment : XpPreferenceFragment() { @@ -24,8 +25,8 @@ open class SettingsFragment : XpPreferenceFragment() { lateinit var subscriptions: CompositeSubscription - private val iconTint by lazy { ContextCompat.getColorStateList( - context, Util.resolveResourceId(context, R.attr.colorAccent, 0)) + private val iconTint by lazy { ContextCompat.getColorStateList(context, + context.theme.getResourceId(R.attr.colorAccent, 0)) } override final fun onCreatePreferences2(savedState: Bundle?, rootKey: String?) { @@ -60,6 +61,7 @@ open class SettingsFragment : XpPreferenceFragment() { @CallSuper override fun onViewCreated(view: View, savedState: Bundle?) { + super.onViewCreated(view, savedState) listView.isFocusable = false } @@ -84,4 +86,8 @@ open class SettingsFragment : XpPreferenceFragment() { "about_screen" to R.drawable.ic_help_black_24dp ) + protected inline fun bindPref(resId: Int): Lazy { + return lazy { findPreference(getString(resId)) as T } + } + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralFragment.kt index 49d0ed2e5..3de6714ee 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralFragment.kt @@ -6,7 +6,8 @@ import android.support.v7.preference.PreferenceFragmentCompat import android.support.v7.preference.XpPreferenceFragment import android.view.View import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.library.LibraryUpdateAlarm +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.library.LibraryUpdateTrigger import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.util.plusAssign import eu.kanade.tachiyomi.widget.preference.IntListPreference @@ -14,6 +15,7 @@ import eu.kanade.tachiyomi.widget.preference.LibraryColumnsDialog import eu.kanade.tachiyomi.widget.preference.SimpleDialogPreference import net.xpece.android.support.preference.MultiSelectListPreference import rx.Observable +import rx.android.schedulers.AndroidSchedulers import uy.kohesive.injekt.injectLazy class SettingsGeneralFragment : SettingsFragment(), @@ -30,22 +32,17 @@ class SettingsGeneralFragment : SettingsFragment(), private val preferences: PreferencesHelper by injectLazy() + private val db: DatabaseHelper by injectLazy() - val columnsPreference by lazy { - findPreference(getString(R.string.pref_library_columns_dialog_key)) as SimpleDialogPreference - } + val columnsPreference: SimpleDialogPreference by bindPref(R.string.pref_library_columns_dialog_key) - val updateInterval by lazy { - findPreference(getString(R.string.pref_library_update_interval_key)) as IntListPreference - } + val updateInterval: IntListPreference by bindPref(R.string.pref_library_update_interval_key) - val updateRestriction by lazy { - findPreference(getString(R.string.pref_library_update_restriction_key)) as MultiSelectListPreference - } + val updateRestriction: MultiSelectListPreference by bindPref(R.string.pref_library_update_restriction_key) - val themePreference by lazy { - findPreference(getString(R.string.pref_theme_key)) as IntListPreference - } + val themePreference: IntListPreference by bindPref(R.string.pref_theme_key) + + val categoryUpdate: MultiSelectListPreference by bindPref(R.string.pref_library_update_categories_key) override fun onViewCreated(view: View, savedState: Bundle?) { super.onViewCreated(view, savedState) @@ -60,10 +57,44 @@ class SettingsGeneralFragment : SettingsFragment(), .subscribe { updateColumnsSummary(it.first, it.second) } updateInterval.setOnPreferenceChangeListener { preference, newValue -> - LibraryUpdateAlarm.startAlarm(activity, (newValue as String).toInt()) + val interval = (newValue as String).toInt() + if (interval > 0) + LibraryUpdateTrigger.setupTask(context, interval) + else + LibraryUpdateTrigger.cancelTask(context) + true } + updateRestriction.setOnPreferenceChangeListener { preference, newValue -> + // Post to event looper to allow the preference to be updated. + subscriptions += Observable.fromCallable { + LibraryUpdateTrigger.setupTask(context) + }.subscribeOn(AndroidSchedulers.mainThread()).subscribe() + + true + } + + val dbCategories = db.getCategories().executeAsBlocking() + categoryUpdate.apply { + entries = dbCategories.map { it.name }.toTypedArray() + entryValues = dbCategories.map { it.id.toString() }.toTypedArray() + } + + subscriptions += preferences.libraryUpdateCategories().asObservable() + .subscribe { + val selectedCategories = it + .mapNotNull { id -> dbCategories.find { it.id == id.toInt() } } + .sortedBy { it.order } + + val summary = if (selectedCategories.isEmpty()) + getString(R.string.all) + else + selectedCategories.joinToString { it.name } + + categoryUpdate.summary = summary + } + themePreference.setOnPreferenceChangeListener { preference, newValue -> (activity as SettingsActivity).parentFlags = SettingsActivity.FLAG_THEME_CHANGED activity.recreate() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesFragment.kt index 56147e256..e29c5369d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesFragment.kt @@ -1,21 +1,19 @@ package eu.kanade.tachiyomi.ui.setting import android.content.Intent +import android.graphics.drawable.Drawable import android.os.Bundle -import android.support.v7.preference.Preference -import android.support.v7.preference.PreferenceGroup import android.support.v7.preference.XpPreferenceFragment import android.view.View import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault -import eu.kanade.tachiyomi.data.source.Source import eu.kanade.tachiyomi.data.source.SourceManager import eu.kanade.tachiyomi.data.source.getLanguages -import eu.kanade.tachiyomi.data.source.online.LoginSource -import eu.kanade.tachiyomi.util.plusAssign -import eu.kanade.tachiyomi.widget.preference.LoginPreference +import eu.kanade.tachiyomi.widget.preference.LoginCheckBoxPreference import eu.kanade.tachiyomi.widget.preference.SourceLoginDialog -import net.xpece.android.support.preference.MultiSelectListPreference +import eu.kanade.tachiyomi.widget.preference.SwitchPreferenceCategory +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy class SettingsSourcesFragment : SettingsFragment() { @@ -32,59 +30,105 @@ class SettingsSourcesFragment : SettingsFragment() { private val preferences: PreferencesHelper by injectLazy() - private val sourceManager: SourceManager by injectLazy() + private val onlineSources by lazy { Injekt.get().getOnlineSources() } - val languagesPref by lazy { findPreference("pref_source_languages") as MultiSelectListPreference } - - val sourcesPref by lazy { findPreference("pref_sources") as PreferenceGroup } + override fun setDivider(divider: Drawable?) { + super.setDivider(null) + } override fun onViewCreated(view: View, savedState: Bundle?) { super.onViewCreated(view, savedState) + // Remove dummy preference + preferenceScreen.removeAll() + + // Get the list of active language codes. + val activeLangsCodes = preferences.enabledLanguages().getOrDefault() + + // Get the list of languages ordered by name. val langs = getLanguages().sortedBy { it.lang } - val entryKeys = langs.map { it.code } - languagesPref.entries = langs.map { it.lang }.toTypedArray() - languagesPref.entryValues = entryKeys.toTypedArray() - languagesPref.values = preferences.enabledLanguages().getOrDefault() + // Order first by active languages, then inactive ones + val orderedLangs = langs.filter { it.code in activeLangsCodes } + + langs.filterNot { it.code in activeLangsCodes } - subscriptions += preferences.enabledLanguages().asObservable() - .subscribe { languages -> - sourcesPref.removeAll() - - val enabledSources = sourceManager.getOnlineSources() - .filter { it.lang.code in languages } - - for (source in enabledSources.filterIsInstance(LoginSource::class.java)) { - val pref = createLoginSourceEntry(source) - sourcesPref.addPreference(pref) - } - - // Hide category if it doesn't have any child - sourcesPref.isVisible = sourcesPref.preferenceCount > 0 + orderedLangs.forEach { lang -> + // Create a preference group and set initial state and change listener + SwitchPreferenceCategory(context).apply { + preferenceScreen.addPreference(this) + title = lang.lang + isPersistent = false + if (lang.code in activeLangsCodes) { + setChecked(true) + addLanguageSources(this) } + + setOnPreferenceChangeListener { preference, any -> + val checked = any as Boolean + val current = preferences.enabledLanguages().getOrDefault() + if (!checked) { + preferences.enabledLanguages().set(current - lang.code) + removeAll() + } else { + preferences.enabledLanguages().set(current + lang.code) + addLanguageSources(this) + } + true + } + } + } } - fun createLoginSourceEntry(source: Source): Preference { - return LoginPreference(preferenceManager.context).apply { - key = preferences.keys.sourceUsername(source.id) - title = source.toString() + /** + * Adds the source list for the given group (language). + * + * @param group the language category. + */ + private fun addLanguageSources(group: SwitchPreferenceCategory) { + val sources = onlineSources.filter { it.lang.lang == group.title }.sortedBy { it.name } + val hiddenCatalogues = preferences.hiddenCatalogues().getOrDefault() + + sources.forEach { source -> + val sourcePreference = LoginCheckBoxPreference(context, source).apply { + val id = source.id.toString() + title = source.name + key = getSourceKey(source.id) + isPersistent = false + isChecked = id !in hiddenCatalogues + + setOnPreferenceChangeListener { preference, any -> + val checked = any as Boolean + val current = preferences.hiddenCatalogues().getOrDefault() + + preferences.hiddenCatalogues().set(if (checked) + current - id + else + current + id) + + true + } + + setOnLoginClickListener { + val fragment = SourceLoginDialog.newInstance(source) + fragment.setTargetFragment(this@SettingsSourcesFragment, SOURCE_CHANGE_REQUEST) + fragment.show(fragmentManager, null) + } - setOnPreferenceClickListener { - val fragment = SourceLoginDialog.newInstance(source) - fragment.setTargetFragment(this@SettingsSourcesFragment, SOURCE_CHANGE_REQUEST) - fragment.show(fragmentManager, null) - true } + group.addPreference(sourcePreference) } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode == SOURCE_CHANGE_REQUEST) { - val pref = findPreference(preferences.keys.sourceUsername(resultCode)) as? LoginPreference + val pref = findPreference(getSourceKey(resultCode)) as? LoginCheckBoxPreference pref?.notifyChanged() } } + private fun getSourceKey(sourceId: Int): String { + return "source_$sourceId" + } + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSyncFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSyncFragment.kt deleted file mode 100644 index 6e6c595b5..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSyncFragment.kt +++ /dev/null @@ -1,61 +0,0 @@ -package eu.kanade.tachiyomi.ui.setting - -import android.content.Intent -import android.os.Bundle -import android.support.v7.preference.PreferenceCategory -import android.support.v7.preference.XpPreferenceFragment -import android.view.View -import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.widget.preference.LoginPreference -import eu.kanade.tachiyomi.widget.preference.MangaSyncLoginDialog -import uy.kohesive.injekt.injectLazy - -class SettingsSyncFragment : SettingsFragment() { - - companion object { - const val SYNC_CHANGE_REQUEST = 121 - - fun newInstance(rootKey: String): SettingsSyncFragment { - val args = Bundle() - args.putString(XpPreferenceFragment.ARG_PREFERENCE_ROOT, rootKey) - return SettingsSyncFragment().apply { arguments = args } - } - } - - private val syncManager: MangaSyncManager by injectLazy() - - private val preferences: PreferencesHelper by injectLazy() - - val syncCategory by lazy { findPreference("pref_category_manga_sync_accounts") as PreferenceCategory } - - override fun onViewCreated(view: View, savedState: Bundle?) { - super.onViewCreated(view, savedState) - - val themedContext = preferenceManager.context - - for (sync in syncManager.services) { - val pref = LoginPreference(themedContext).apply { - key = preferences.keys.syncUsername(sync.id) - title = sync.name - - setOnPreferenceClickListener { - val fragment = MangaSyncLoginDialog.newInstance(sync) - fragment.setTargetFragment(this@SettingsSyncFragment, SYNC_CHANGE_REQUEST) - fragment.show(fragmentManager, null) - true - } - } - - syncCategory.addPreference(pref) - } - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode == SYNC_CHANGE_REQUEST) { - val pref = findPreference(preferences.keys.syncUsername(resultCode)) as? LoginPreference - pref?.notifyChanged() - } - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/RxPager.kt b/app/src/main/java/eu/kanade/tachiyomi/util/RxPager.kt deleted file mode 100644 index c46d68e1a..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/util/RxPager.kt +++ /dev/null @@ -1,21 +0,0 @@ -package eu.kanade.tachiyomi.util - -import android.util.Pair -import rx.Observable -import rx.subjects.PublishSubject - -class RxPager { - - private val results = PublishSubject.create>() - private var requestedCount: Int = 0 - - fun results(): Observable>> { - requestedCount = 0 - return results.map { Pair(requestedCount++, it) } - } - - fun request(networkObservable: (Int) -> Observable>) = - networkObservable(requestedCount).doOnNext { results.onNext(it) } - -} - diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/ThemeExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/ThemeExtensions.kt index 81c1988fe..da0f90282 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/ThemeExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/ThemeExtensions.kt @@ -2,18 +2,26 @@ package eu.kanade.tachiyomi.util import android.content.res.Resources import android.graphics.drawable.Drawable +import android.support.annotation.AttrRes import android.support.annotation.StringRes -fun Resources.Theme.getResourceColor(@StringRes resource: Int) : Int { - val typedArray = this.obtainStyledAttributes(intArrayOf(resource)) +fun Resources.Theme.getResourceColor(@StringRes resource: Int): Int { + val typedArray = obtainStyledAttributes(intArrayOf(resource)) val attrValue = typedArray.getColor(0, 0) typedArray.recycle() return attrValue } -fun Resources.Theme.getResourceDrawable(@StringRes resource: Int) : Drawable { - val typedArray = this.obtainStyledAttributes(intArrayOf(resource)) +fun Resources.Theme.getResourceDrawable(@StringRes resource: Int): Drawable { + val typedArray = obtainStyledAttributes(intArrayOf(resource)) val attrValue = typedArray.getDrawable(0) typedArray.recycle() return attrValue +} + +fun Resources.Theme.getResourceId(@AttrRes resource: Int, fallback: Int): Int { + val typedArray = obtainStyledAttributes(intArrayOf(resource)) + val attrValue = typedArray.getResourceId(0, fallback) + typedArray.recycle() + return attrValue } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/AutofitRecyclerView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/AutofitRecyclerView.kt index 11a8e1140..62cc4b04c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/AutofitRecyclerView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/AutofitRecyclerView.kt @@ -37,8 +37,8 @@ class AutofitRecyclerView @JvmOverloads constructor(context: Context, attrs: Att override fun onMeasure(widthSpec: Int, heightSpec: Int) { super.onMeasure(widthSpec, heightSpec) if (spanCount == 0 && columnWidth > 0) { - val spanCount = Math.max(1, measuredWidth / columnWidth) - manager.spanCount = spanCount + val count = Math.max(1, measuredWidth / columnWidth) + spanCount = count } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/NegativeSeekBar.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/NegativeSeekBar.kt new file mode 100644 index 000000000..44c9ee150 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/NegativeSeekBar.kt @@ -0,0 +1,61 @@ +package eu.kanade.tachiyomi.widget + +import android.content.Context +import android.util.AttributeSet +import android.widget.SeekBar +import eu.kanade.tachiyomi.R + + +class NegativeSeekBar @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + SeekBar(context, attrs) { + + private var minValue: Int = 0 + private var maxValue: Int = 0 + private var listener: OnSeekBarChangeListener? = null + + init { + val styledAttributes = context.obtainStyledAttributes( + attrs, + R.styleable.NegativeSeekBar, 0, 0) + + try { + setMinSeek(styledAttributes.getInt(R.styleable.NegativeSeekBar_min_seek, 0)) + setMaxSeek(styledAttributes.getInt(R.styleable.NegativeSeekBar_max_seek, 0)) + } finally { + styledAttributes.recycle() + } + + super.setOnSeekBarChangeListener(object : OnSeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar?, value: Int, fromUser: Boolean) { + listener?.let { it.onProgressChanged(seekBar, minValue + value, fromUser) } + } + + override fun onStartTrackingTouch(p0: SeekBar?) { + listener?.let { it.onStartTrackingTouch(p0) } + } + + override fun onStopTrackingTouch(p0: SeekBar?) { + listener?.let { it.onStopTrackingTouch(p0) } + } + }) + } + + override fun setProgress(progress: Int) { + super.setProgress(Math.abs(minValue) + progress) + } + + fun setMinSeek(minValue: Int) { + this.minValue = minValue + max = (this.maxValue - this.minValue) + } + + fun setMaxSeek(maxValue: Int) { + this.maxValue = maxValue + max = (this.maxValue - this.minValue) + } + + override fun setOnSeekBarChangeListener(listener: OnSeekBarChangeListener?) { + this.listener = listener + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/RecyclerViewPagerAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/RecyclerViewPagerAdapter.kt new file mode 100644 index 000000000..6d0044fe8 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/RecyclerViewPagerAdapter.kt @@ -0,0 +1,42 @@ +package eu.kanade.tachiyomi.widget + +import android.support.v4.view.PagerAdapter +import android.view.View +import android.view.ViewGroup +import java.util.* + +abstract class RecyclerViewPagerAdapter : PagerAdapter() { + + private val pool = Stack() + + var recycle = true + set(value) { + if (!value) pool.clear() + field = value + } + + protected abstract fun createView(container: ViewGroup): View + + protected abstract fun bindView(view: View, position: Int) + + protected open fun recycleView(view: View, position: Int) {} + + override fun instantiateItem(container: ViewGroup, position: Int): Any { + val view = if (pool.isNotEmpty()) pool.pop() else createView(container) + bindView(view, position) + container.addView(view) + return view + } + + override fun destroyItem(container: ViewGroup, position: Int, obj: Any) { + val view = obj as View + recycleView(view, position) + container.removeView(view) + if (recycle) pool.push(view) + } + + override fun isViewFromObject(view: View, obj: Any): Boolean { + return view === obj + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/SimpleSeekBarListener.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/SimpleSeekBarListener.kt index f0599159d..77f815bd3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/SimpleSeekBarListener.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/SimpleSeekBarListener.kt @@ -1,11 +1,13 @@ package eu.kanade.tachiyomi.widget - import android.widget.SeekBar open class SimpleSeekBarListener : SeekBar.OnSeekBarChangeListener { - override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {} + override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) { + } - override fun onStartTrackingTouch(seekBar: SeekBar) {} + override fun onStartTrackingTouch(seekBar: SeekBar) { + } - override fun onStopTrackingTouch(seekBar: SeekBar) {} + override fun onStopTrackingTouch(seekBar: SeekBar) { + } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/ViewPagerAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/ViewPagerAdapter.kt new file mode 100644 index 000000000..c2c21a66b --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/ViewPagerAdapter.kt @@ -0,0 +1,30 @@ +package eu.kanade.tachiyomi.widget + +import android.support.v4.view.PagerAdapter +import android.view.View +import android.view.ViewGroup + +abstract class ViewPagerAdapter : PagerAdapter() { + + protected abstract fun createView(container: ViewGroup, position: Int): View + + protected open fun destroyView(container: ViewGroup, position: Int, view: View) { + } + + override fun instantiateItem(container: ViewGroup, position: Int): Any { + val view = createView(container, position) + container.addView(view) + return view + } + + override fun destroyItem(container: ViewGroup, position: Int, obj: Any) { + val view = obj as View + destroyView(container, position, view) + container.removeView(view) + } + + override fun isViewFromObject(view: View, obj: Any): Boolean { + return view === obj + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginCheckBoxPreference.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginCheckBoxPreference.kt new file mode 100644 index 000000000..1d4ef5862 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginCheckBoxPreference.kt @@ -0,0 +1,56 @@ +package eu.kanade.tachiyomi.widget.preference + +import android.content.Context +import android.graphics.Color +import android.support.v7.preference.PreferenceViewHolder +import android.util.AttributeSet +import android.view.View +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.source.online.LoginSource +import eu.kanade.tachiyomi.data.source.online.OnlineSource +import eu.kanade.tachiyomi.util.setVectorCompat +import kotlinx.android.synthetic.main.pref_item_source.view.* +import net.xpece.android.support.preference.CheckBoxPreference + +class LoginCheckBoxPreference @JvmOverloads constructor( + context: Context, + val source: OnlineSource, + attrs: AttributeSet? = null +) : CheckBoxPreference(context, attrs) { + + init { + layoutResource = R.layout.pref_item_source + } + + private var onLoginClick: () -> Unit = {} + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + val loginFrame = holder.itemView.login_frame + if (source is LoginSource) { + val tint = if (source.isLogged()) + Color.argb(255, 76, 175, 80) + else + Color.argb(97, 0, 0, 0) + + holder.itemView.login.setVectorCompat(R.drawable.ic_account_circle_black_24dp, tint) + + loginFrame.visibility = View.VISIBLE + loginFrame.setOnClickListener { + onLoginClick() + } + } else { + loginFrame.visibility = View.GONE + } + } + + fun setOnLoginClickListener(block: () -> Unit) { + onLoginClick = block + } + + // Make method public + override public fun notifyChanged() { + super.notifyChanged() + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/MangaSyncLoginDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/MangaSyncLoginDialog.kt deleted file mode 100644 index 63f20de60..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/MangaSyncLoginDialog.kt +++ /dev/null @@ -1,71 +0,0 @@ -package eu.kanade.tachiyomi.widget.preference - -import android.os.Bundle -import android.view.View -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager -import eu.kanade.tachiyomi.data.mangasync.MangaSyncService -import eu.kanade.tachiyomi.util.toast -import kotlinx.android.synthetic.main.pref_account_login.view.* -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers -import uy.kohesive.injekt.injectLazy - -class MangaSyncLoginDialog : LoginDialogPreference() { - - companion object { - - fun newInstance(sync: MangaSyncService): LoginDialogPreference { - val fragment = MangaSyncLoginDialog() - val bundle = Bundle(1) - bundle.putInt("key", sync.id) - fragment.arguments = bundle - return fragment - } - } - - val syncManager: MangaSyncManager by injectLazy() - - lateinit var sync: MangaSyncService - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - val syncId = arguments.getInt("key") - sync = syncManager.getService(syncId)!! - } - - override fun setCredentialsOnView(view: View) = with(view) { - dialog_title.text = getString(R.string.login_title, sync.name) - username.setText(sync.getUsername()) - password.setText(sync.getPassword()) - } - - override fun checkLogin() { - requestSubscription?.unsubscribe() - - v?.apply { - if (username.text.length == 0 || password.text.length == 0) - return - - login.progress = 1 - val user = username.text.toString() - val pass = password.text.toString() - - requestSubscription = sync.login(user, pass) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ error -> - sync.logout() - login.progress = -1 - login.setText(R.string.unknown_error) - }, { - sync.saveCredentials(user, pass) - dialog.dismiss() - context.toast(R.string.login_success) - }) - - } - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/SwitchPreferenceCategory.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/SwitchPreferenceCategory.kt new file mode 100644 index 000000000..bf54136f1 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/SwitchPreferenceCategory.kt @@ -0,0 +1,137 @@ +package eu.kanade.tachiyomi.widget.preference + +import android.annotation.TargetApi +import android.content.Context +import android.content.res.TypedArray +import android.os.Build +import android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH +import android.support.v7.preference.PreferenceViewHolder +import android.support.v7.widget.SwitchCompat +import android.util.AttributeSet +import android.view.View +import android.widget.Checkable +import android.widget.CompoundButton +import android.widget.Switch +import eu.kanade.tachiyomi.util.getResourceColor +import net.xpece.android.support.preference.PreferenceCategory +import net.xpece.android.support.preference.R + +class SwitchPreferenceCategory @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null) +: PreferenceCategory( + context, + attrs, + R.attr.switchPreferenceCompatStyle, + R.style.Preference_Material_SwitchPreferenceCompat), +CompoundButton.OnCheckedChangeListener { + + init { + setTitleTextColor(context.theme.getResourceColor(R.attr.colorAccent)) + } + + private var mChecked = false + + private var mCheckedSet = false + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + syncSwitchView(holder) + } + + private fun syncSwitchView(holder: PreferenceViewHolder) { + val switchView = holder.findViewById(R.id.switchWidget) + syncSwitchView(switchView) + } + + @TargetApi(ICE_CREAM_SANDWICH) + private fun syncSwitchView(view: View) { + if (view is Checkable) { + val isChecked = view.isChecked + if (isChecked == mChecked) return + + if (view is SwitchCompat) { + view.setOnCheckedChangeListener(null) + } else if (NATIVE_SWITCH_CAPABLE && view is Switch) { + view.setOnCheckedChangeListener(null) + } + + view.toggle() + + if (view is SwitchCompat) { + view.setOnCheckedChangeListener(this) + } else if (NATIVE_SWITCH_CAPABLE && view is Switch) { + view.setOnCheckedChangeListener(this) + } + } + } + + override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) { + if (!callChangeListener(isChecked)) { + buttonView.isChecked = !isChecked + } else { + setChecked(isChecked) + } + } + + override fun onClick() { + super.onClick() + + val newValue = !isChecked() + if (callChangeListener(newValue)) { + setChecked(newValue) + } + } + + /** + * Sets the checked state and saves it to the [SharedPreferences]. + * + * @param checked The checked state. + */ + fun setChecked(checked: Boolean) { + // Always persist/notify the first time; don't assume the field's default of false. + val changed = mChecked != checked + if (changed || !mCheckedSet) { + mChecked = checked + mCheckedSet = true + persistBoolean(checked) + if (changed) { + notifyDependencyChange(shouldDisableDependents()) + notifyChanged() + } + } + } + + /** + * Returns the checked state. + * + * @return The checked state. + */ + fun isChecked(): Boolean { + return mChecked + } + + override fun isEnabled(): Boolean { + return true + } + + override fun shouldDisableDependents(): Boolean { + return false + } + + override fun onGetDefaultValue(a: TypedArray, index: Int): Any { + return a.getBoolean(index, false) + } + + override fun onSetInitialValue(restoreValue: Boolean, defaultValue: Any?) { + setChecked(if (restoreValue) + getPersistedBoolean(mChecked) + else + defaultValue as Boolean) + } + + companion object { + private val NATIVE_SWITCH_CAPABLE = Build.VERSION.SDK_INT >= ICE_CREAM_SANDWICH + } + +} \ No newline at end of file diff --git a/app/src/main/res/drawable/filter_mock.png b/app/src/main/res/drawable/filter_mock.png new file mode 100644 index 000000000..a6d6a0c00 Binary files /dev/null and b/app/src/main/res/drawable/filter_mock.png differ diff --git a/app/src/main/res/drawable/ic_account_circle_black_24dp.xml b/app/src/main/res/drawable/ic_account_circle_black_24dp.xml new file mode 100644 index 000000000..76785806d --- /dev/null +++ b/app/src/main/res/drawable/ic_account_circle_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_brightness_4_white_24dp.xml b/app/src/main/res/drawable/ic_brightness_4_white_24dp.xml new file mode 100644 index 000000000..09f16c1d3 --- /dev/null +++ b/app/src/main/res/drawable/ic_brightness_4_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_brightness_5_black_24dp.xml b/app/src/main/res/drawable/ic_brightness_5_black_24dp.xml new file mode 100644 index 000000000..54301c0aa --- /dev/null +++ b/app/src/main/res/drawable/ic_brightness_5_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_home_white_24dp.xml b/app/src/main/res/drawable/ic_home_white_24dp.xml new file mode 100644 index 000000000..fafc05e0c --- /dev/null +++ b/app/src/main/res/drawable/ic_home_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_share_white_24dp.xml b/app/src/main/res/drawable/ic_share_white_24dp.xml index 034ea67d5..c5027c659 100644 --- a/app/src/main/res/drawable/ic_share_white_24dp.xml +++ b/app/src/main/res/drawable/ic_share_white_24dp.xml @@ -1,9 +1,9 @@ + android:width="24dp" + android:height="24dp" + android:viewportHeight="24.0" + android:viewportWidth="24.0"> + android:fillColor="#FFFFFFFF" + android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z" /> diff --git a/app/src/main/res/drawable/ic_watch_later_black_24dp.xml b/app/src/main/res/drawable/ic_watch_later_black_24dp.xml new file mode 100644 index 000000000..6032098bd --- /dev/null +++ b/app/src/main/res/drawable/ic_watch_later_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/mask_star.png b/app/src/main/res/drawable/mask_star.png new file mode 100644 index 000000000..5d3c11953 Binary files /dev/null and b/app/src/main/res/drawable/mask_star.png differ diff --git a/app/src/main/res/layout/activity_reader.xml b/app/src/main/res/layout/activity_reader.xml index 8812abbbd..0cbec3f42 100644 --- a/app/src/main/res/layout/activity_reader.xml +++ b/app/src/main/res/layout/activity_reader.xml @@ -105,4 +105,10 @@ android:layout_height="match_parent" android:visibility="gone"/> + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_reader_custom_filter.xml b/app/src/main/res/layout/dialog_reader_custom_filter.xml new file mode 100644 index 000000000..4abe82fdd --- /dev/null +++ b/app/src/main/res/layout/dialog_reader_custom_filter.xml @@ -0,0 +1,257 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_reader_settings.xml b/app/src/main/res/layout/dialog_reader_settings.xml index f78b724ae..67244208d 100644 --- a/app/src/main/res/layout/dialog_reader_settings.xml +++ b/app/src/main/res/layout/dialog_reader_settings.xml @@ -1,9 +1,9 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_catalogue.xml b/app/src/main/res/layout/fragment_catalogue.xml index 1f5ec2e4e..d862a45b7 100644 --- a/app/src/main/res/layout/fragment_catalogue.xml +++ b/app/src/main/res/layout/fragment_catalogue.xml @@ -1,48 +1,55 @@ - + - + - - - + android:layout_gravity="center_vertical|center_horizontal" + android:visibility="gone"/> - + android:layout_height="0dp" + android:layout_weight="1"> + - + - + - + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_library.xml b/app/src/main/res/layout/fragment_library.xml index f603e3d3e..2edae65d2 100644 --- a/app/src/main/res/layout/fragment_library.xml +++ b/app/src/main/res/layout/fragment_library.xml @@ -3,11 +3,9 @@ android:layout_height="match_parent" android:orientation="vertical"> - - - + diff --git a/app/src/main/res/layout/fragment_library_category.xml b/app/src/main/res/layout/fragment_library_category.xml index 46801572a..a0614ea62 100644 --- a/app/src/main/res/layout/fragment_library_category.xml +++ b/app/src/main/res/layout/fragment_library_category.xml @@ -1,22 +1,14 @@ - + - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_catalogue_grid.xml b/app/src/main/res/layout/item_catalogue_grid.xml index 4027c126c..6e7fae66d 100644 --- a/app/src/main/res/layout/item_catalogue_grid.xml +++ b/app/src/main/res/layout/item_catalogue_grid.xml @@ -19,7 +19,8 @@ android:layout_height="match_parent" android:background="?android:attr/colorBackground" tools:background="?android:attr/colorBackground" - tools:src="@mipmap/ic_launcher" /> + tools:src="@mipmap/ic_launcher" + tools:ignore="ContentDescription" /> + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_library_list.xml b/app/src/main/res/layout/item_library_list.xml new file mode 100644 index 000000000..d846ab9d0 --- /dev/null +++ b/app/src/main/res/layout/item_library_list.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_pager_reader.xml b/app/src/main/res/layout/item_pager_reader.xml index 4fefdc9b1..3b5f3d8c9 100644 --- a/app/src/main/res/layout/item_pager_reader.xml +++ b/app/src/main/res/layout/item_pager_reader.xml @@ -1,6 +1,6 @@ - @@ -40,4 +40,4 @@ android:layout_gravity="center" android:visibility="gone"/> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/library_grid_recycler.xml b/app/src/main/res/layout/library_grid_recycler.xml new file mode 100644 index 000000000..fa4563430 --- /dev/null +++ b/app/src/main/res/layout/library_grid_recycler.xml @@ -0,0 +1,10 @@ + + diff --git a/app/src/main/res/layout/library_list_recycler.xml b/app/src/main/res/layout/library_list_recycler.xml new file mode 100644 index 000000000..8f5dc91b3 --- /dev/null +++ b/app/src/main/res/layout/library_list_recycler.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/pref_item_source.xml b/app/src/main/res/layout/pref_item_source.xml new file mode 100644 index 000000000..27ff9b02e --- /dev/null +++ b/app/src/main/res/layout/pref_item_source.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/catalogue_list.xml b/app/src/main/res/menu/catalogue_list.xml index d7d4d844f..9ff61236d 100644 --- a/app/src/main/res/menu/catalogue_list.xml +++ b/app/src/main/res/menu/catalogue_list.xml @@ -9,6 +9,12 @@ app:showAsAction="collapseActionView|ifRoom" app:actionViewClass="android.support.v7.widget.SearchView"/> + + + android:title="@string/action_filter_read"/> + diff --git a/app/src/main/res/menu/library.xml b/app/src/main/res/menu/library.xml index bff4820cf..eb9285fad 100644 --- a/app/src/main/res/menu/library.xml +++ b/app/src/main/res/menu/library.xml @@ -2,10 +2,17 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" tools:context=".MainActivity"> + + - - - Idiomas - Seleciona em que idiomas mostras as fontes - Contas + Desativado + Último capítulo + Segundo capítulo antes do último + Terceiro capítulo antes do último + Quarto capítulo antes do último + Quinto capítulo antes do último Serviços @@ -165,11 +166,11 @@ Limpar redundância da base de dados Eliminar manga não presente na biblioteca Capítulos lidos e progresso em manga que não está na biblioteca será perdido. Deseja continuar? - Entradas eliminar + Eliminar entradas Mostrar avisos Mostrar mensagens de avisos durante a sincronização Recodificar imagens - Permitir recodificação se as imagens não poderem ser descodificadas. Para melhores resultados utilize Skia + Permitir recodificação se as imagens não puderam ser descodificadas. Para melhores resultados utilize Skia Versão @@ -180,28 +181,28 @@ Enviar relatórios de erro Ajuda a corrigir qualquer erro. Não serão enviados dados sensíveis - Iniciar sessão para %1$s Nome de utilizador - Password - Mostrar password + Palavra-passe + Mostrar palavra-passe Iniciar sessão Início de sessão bem sucedido Início de sessão falhado Erro desconhecido - - Titulo ou autor… + + Título ou autor… A atualizar categoria Eliminar manga selecionada? Esta fonte necessita que inicies a sessão Selecionar a fonte + Por favor habilite pelo menos uma fonte valida - Info + Informações Descrição Em curso Desconhecido @@ -217,7 +218,7 @@ Capítulos - Sem titulo + Sem título Capítulo %1$s Transferido Em fila @@ -225,7 +226,7 @@ A Transferir (%1$d/%2$d) Erro Erro ao obter capítulos - Mostrar titulo + Mostrar título Mostrar número de capítulo Modo de organização Por fonte @@ -236,7 +237,7 @@ Transferir próximos 10 capítulos Transferir tudo Transferir não lidos - Tem a certeza que deseja eliminar os capítulos selecionados? + Eliminar os capítulos selecionados? A ler @@ -245,14 +246,13 @@ Em espera Planeada a leitura Avaliação - Titulo… + Título… Estado Capítulos - Isto irá remover a data de leitura deste capítulo. Continuar? - Repor todos os capítulos deste manga - + Esta ação irá remover a data de leitura deste capítulo. Continuar? + Repor todos os capítulos desta manga A transferir… @@ -261,7 +261,7 @@ Capítulo %1$s Capítulo seguinte não encontrado Capítulo anterior não encontrado - Imagem não pode ser carregada.\nPor favor tente alterar o descodificador de imagem ou selecione uma das seguintes opções + Imagem não pôde ser carregada.\nPor favor tente alterar o descodificador de imagem ou selecione uma das seguintes opções Atualizar último capítulo lido a %1$d nos serviços ativos? Visualizador para esta série @@ -270,21 +270,21 @@ Restaurar Cópia de segurança em curso. Por favor aguarde… Cópia de segurança restaurada com sucesso - Restaurando cópia de segurança. Por favor aguarde… + A restaurar cópia de segurança. Por favor aguarde… - %1$s - Ch.%2$s + %1$s - Cap.%2$s - + Ocorreu um erro ao transferir capítulos. Poderá tentar novamente na secção das transferências - + Progresso da atualização: %1$d/%2$d Atualização completa Ocorreu um erro inesperado ao atualizar a biblioteca Sem novos capítulos encontrados Novos capítulos encontrados para: - Falha ao atualizar ao manga: + Falha ao atualizar a manga: Por favor adicione a manga à sua biblioteca antes de fazer isto Sincronização cancelada O dispositivo não está ligado ao carregador @@ -311,8 +311,8 @@ Atualização disponível - Imagem de capa de manga - Imagem de capa de manga + Imagem de capa da manga + Imagem de capa da manga Sem transferências @@ -327,4 +327,4 @@ Página não carregada Sem ligação de Wi-Fi disponível - \ No newline at end of file + diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 3c7df32ef..1e9355cb1 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -48,6 +48,24 @@ 3 + + @string/disabled + @string/last_read_chapter + @string/second_to_last + @string/third_to_last + @string/fourth_to_last + @string/fifth_to_last + + + + -1 + 0 + 1 + 2 + 3 + 4 + + @string/rapid_decoder @string/skia_decoder diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 6af90d376..0d37df9c3 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -17,6 +17,11 @@ + + + + + diff --git a/app/src/main/res/values/keys.xml b/app/src/main/res/values/keys.xml index c57dadaaf..62cfab32d 100644 --- a/app/src/main/res/values/keys.xml +++ b/app/src/main/res/values/keys.xml @@ -10,10 +10,12 @@ pref_category_about_key pref_category_sources_key + pref_display_library_as_list pref_library_columns_dialog_key pref_library_columns_portrait_key pref_library_columns_landscape_key pref_library_update_interval_key + library_update_categories pref_update_only_non_completed_key pref_auto_update_manga_sync_key pref_ask_update_manga_sync_key @@ -31,6 +33,9 @@ pref_keep_screen_on_key pref_custom_brightness_key custom_brightness_value + pref_color_filter_key + color_filter_value + pref_red_filter_value pref_reader_theme_key pref_image_decoder_key reader_volume_keys @@ -41,20 +46,20 @@ pref_download_directory_key pref_download_slots_key + remove_after_read_slots pref_download_only_over_wifi_key pref_remove_after_marked_as_read_key pref_category_remove_after_read_key - pref_remove_after_read_key - pref_remove_after_read_previous_key - last_used_category pref_source_languages + category_manga_sync_accounts pref_clear_chapter_cache_key pref_clear_database_key pref_clear_cookies_key + refresh_library_metadata pref_version pref_build_time diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3b48da080..a891a7135 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -8,8 +8,9 @@ Download queue My library Recently read - Recent updates Galleries + Library updates + Latest updates Categories Selected: %1$d Backup @@ -19,6 +20,7 @@ Filter Downloaded Unread + Read Remove filter Search Select all @@ -50,10 +52,13 @@ Remove Resume Open in browser + Add to home screen Change display mode + Set filter Cancel Sort Install + Share Deleting… @@ -84,6 +89,8 @@ Every 12 hours Daily Every 2 days + Categories to include in global update + All Library update restrictions Update only when the conditions are met Wi-Fi @@ -102,6 +109,7 @@ Enable transitions Show page number Use custom brightness + Use custom color filter Keep screen on Navigation Volume keys @@ -135,6 +143,11 @@ Lock Force portrait Force landscape + R + G + B + A + Downloads directory @@ -142,16 +155,13 @@ Only download over Wi-Fi Remove when marked as read Remove after read - Remove after read - Current chapter - Previous chapter Custom directory - - - - Languages - Select the languages to show sources from - Accounts + Disabled + Last read chapter + Second to last chapter + Third to last chapter + Fourth to last chapter + Fifth to last chapter Services @@ -167,8 +177,8 @@ Delete manga and chapters that are not in your library Are you sure? Read chapters and progress of non-library manga will be lost Entries deleted - Show warnings - Show warning messages during library sync + Refresh library metadata + Updates covers, genres, description and manga status information Reencode images Enable reencoding if images can\'t be decoded. Expect best results with Skia @@ -201,6 +211,7 @@ This source requires you to log in Select a source + Please enable at least one valid source Info @@ -216,6 +227,15 @@ Status Source Genres + Share… + Check out %1$s! at %2$s + Circular icon + Rounded icon + Square icon + Star icon + Shortcut title + Icon shape + Failed to create shortcut! Chapters @@ -257,6 +277,7 @@ + Custom filter Downloading… Downloaded %1$d%% Page: %1$d @@ -296,6 +317,7 @@ Select cover image Select backup file + Select shortcut icon New update available! diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 46f79b995..ebd7a695f 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -91,6 +91,10 @@ 16sp + + @@ -107,6 +111,10 @@ 20sp + + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index a38ec375f..bbf56979d 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -36,6 +36,8 @@ @drawable/line_divider_light @color/textColorPrimaryLight @color/dialogLight + ?colorAccent + ?colorAccent