Merge changes.

Various changes.
This commit is contained in:
NerdNumber9 2016-10-18 21:50:48 -04:00
commit 3cafbd9141
140 changed files with 3738 additions and 3093 deletions

View File

@ -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

0
CHANGES.md Normal file
View File

3
app/.gitignore vendored
View File

@ -1,4 +1,5 @@
/build
*iml
*.iml
.idea
custom.gradle
google-services.json

View File

@ -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()
}

View File

@ -1,5 +1,7 @@
-dontobfuscate
-keep class eu.kanade.tachiyomi.**
# OkHttp
-keepattributes Signature
-keepattributes *Annotation*

View File

@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.kanade.tachiyomi">
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="eu.kanade.tachiyomi">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
@ -8,6 +10,8 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE" tools:node="remove" />
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<application
android:name=".App"
@ -18,8 +22,7 @@
android:largeHeap="true"
android:theme="@style/Theme.Tachiyomi" >
<activity
android:name=".ui.main.MainActivity"
android:theme="@style/Theme.BrandedLaunch">
android:name=".ui.main.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
@ -28,7 +31,8 @@
</activity>
<activity
android:name=".ui.manga.MangaActivity"
android:parentActivityName=".ui.main.MainActivity">
android:parentActivityName=".ui.main.MainActivity"
android:exported="true">
</activity>
<activity
android:name=".ui.reader.ReaderActivity"
@ -59,47 +63,33 @@
<service android:name=".data.mangasync.UpdateMangaSyncService"
android:exported="false"/>
<receiver
android:name=".data.library.LibraryUpdateService$SyncOnConnectionAvailable"
android:enabled="false">
<service
android:name=".data.library.LibraryUpdateTrigger"
android:exported="true"
android:permission="com.google.android.gms.permission.BIND_NETWORK_TASK_SERVICE">
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
<action android:name="com.google.android.gms.gcm.ACTION_TASK_READY" />
</intent-filter>
</receiver>
</service>
<receiver
android:name=".data.library.LibraryUpdateService$SyncOnPowerConnected"
android:enabled="false">
<service
android:name=".data.updater.UpdateCheckerService"
android:exported="true"
android:permission="com.google.android.gms.permission.BIND_NETWORK_TASK_SERVICE">
<intent-filter>
<action android:name="android.intent.action.ACTION_POWER_CONNECTED" />
<action android:name="com.google.android.gms.gcm.ACTION_TASK_READY" />
</intent-filter>
</receiver>
</service>
<service android:name=".data.updater.UpdateDownloaderService"
android:exported="false"/>
<receiver android:name=".data.updater.UpdateNotificationReceiver"/>
<receiver
android:name=".data.library.LibraryUpdateService$CancelUpdateReceiver">
</receiver>
<receiver
android:name=".data.updater.UpdateDownloader$InstallOnReceived">
</receiver>
<receiver
android:name=".data.library.LibraryUpdateAlarm">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="eu.kanade.UPDATE_LIBRARY" />
</intent-filter>
</receiver>
<receiver
android:name=".data.updater.UpdateDownloaderAlarm">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="eu.kanade.CHECK_UPDATE"/>
</intent-filter>
</receiver>
<meta-data
android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"
android:value="GlideModule" />

View File

@ -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)
}

View File

@ -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<MangaSync>) = 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()
}

View File

@ -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)
}
}
}

View File

@ -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
}
}

View File

@ -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<PreferencesHelper>().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)
}
}
}

View File

@ -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<Manga> {
val categoryId = intent?.getIntExtra(UPDATE_CATEGORY, -1) ?: -1
fun getMangaToUpdate(intent: Intent): List<Manga> {
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<Manga>): Observable<Manga> {
fun updateChapterList(mangaToUpdate: List<Manga>): Observable<Manga> {
// Initialize the variables holding the progress of the updates.
val count = AtomicInteger(0)
val newUpdates = ArrayList<Manga>()
@ -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<Manga>): Observable<Manga> {
// 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<Manga>()
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.
*/

View File

@ -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<PreferencesHelper>()
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)
}
}
}

View File

@ -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<MangaSyncService>()
fun getService(id: Int) = services.find { it.id == id }
}

View File

@ -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<MangaSync>
abstract fun update(manga: MangaSync): Observable<MangaSync>
abstract fun bind(manga: MangaSync): Observable<MangaSync>
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)
}

View File

@ -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)
}
}
}

View File

@ -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<List<MangaSync>> {
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<List<MangaSync>> {
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<MangaSync> {
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<MangaSync> {
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<MangaSync> {
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()
}
}

View File

@ -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}"

View File

@ -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)
}

View File

@ -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())
}

View File

@ -47,5 +47,4 @@ interface Source {
* @param page the page.
*/
fun fetchImage(page: Page): Observable<Page>
}

View File

@ -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<Source> =
if (DialogLogin.isLoggedIn(context, false))
listOf(EHentai(1, false), EHentai(2, true))
else
listOf(EHentai(1, false))
private fun createSources(): Map<Int, Source> = hashMapOf<Int, Source>().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?")
}

View File

@ -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<MangasPage> = client
.newCall(searchMangaRequest(page, query))
open fun fetchSearchManga(page: MangasPage, query: String, filters: List<Filter>): Observable<MangasPage> = 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<Filter>): 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<Filter>): 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<Filter>)
/**
* Returns an observable containing a page with a list of latest manga.
*/
open fun fetchLatestUpdates(page: MangasPage): Observable<MangasPage> = 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<Filter> = emptyList()
}

View File

@ -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<Filter>) {
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 <a> 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
}

View File

@ -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<Filter>): 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<Filter>) = 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<Filter>) {
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")
}
}
}

View File

@ -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<String, Any?>): RequestableNode {
}
class LatestUpdatesNode(override val map: Map<String, Any?>): 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<String, Any?>): RequestableNode {
val manga_css: String by map

View File

@ -6,7 +6,6 @@ import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import rx.Observable
/**
* Used to connect with the Github API.
*/

View File

@ -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<GithubRelease> {
context.toast(R.string.update_check_look_for_updates)
return service.getLatestVersion()
fun checkForUpdate(): Observable<GithubUpdateResult> {
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()
}
}
}
}

View File

@ -0,0 +1,7 @@
package eu.kanade.tachiyomi.data.updater
sealed class GithubUpdateResult {
class NewUpdate(val release: GithubRelease): GithubUpdateResult()
class NoNewUpdate(): GithubUpdateResult()
}

View File

@ -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)
}
}
}

View File

@ -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<String, Int, UpdateDownloader.DownloadResult>() {
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)
}
}
}
}

View File

@ -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<PreferencesHelper>().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()
})
}
}
}

View File

@ -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())
}
}

View File

@ -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)
}
}
}

View File

@ -41,6 +41,8 @@ class BackupFragment : BaseRxFragment<BackupPresenter>() {
}
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<BackupPresenter>() {
.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) }

View File

@ -27,7 +27,7 @@ abstract class FlexibleViewHolder(view: View,
return true
}
protected fun toggleActivation() {
fun toggleActivation() {
itemView.isActivated = adapter.isSelected(adapterPosition)
}

View File

@ -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<CataloguePresenter>(), FlexibleViewHolder.OnListItemClickListener {
open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), 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<CataloguePresenter>(), FlexibleViewHold
/**
* Query of the search box.
*/
val query: String?
private val query: String
get() = presenter.query
/**
@ -93,11 +94,6 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), 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<CataloguePresenter>(), 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<CataloguePresenter>(), 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<CataloguePresenter>(), 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<CataloguePresenter>(), 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<CataloguePresenter>(), 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<CataloguePresenter>(), 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<CataloguePresenter>(), 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<CataloguePresenter>(), 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<CataloguePresenter>(), FlexibleViewHold
*/
fun onAddPage(page: Int, mangas: List<Manga>) {
hideProgressBar()
if (page == 0) {
if (page == 1) {
adapter.clear()
gridScrollListener.resetScroll()
listScrollListener.resetScroll()
@ -329,12 +341,12 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), 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<CataloguePresenter>(), 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<CataloguePresenter>(), 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()
}
}

View File

@ -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<Filter>): Pager() {
override fun requestNext(transformer: (Observable<MangasPage>) -> Observable<MangasPage>): Observable<MangasPage> {
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 }
}
}

View File

@ -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<CatalogueFragment>() {
open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
/**
* Source manager.
@ -64,14 +66,14 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
private set
/**
* Pager containing a list of manga results.
* Active filters.
*/
private var pager = RxPager<Manga>()
var filters: List<Filter> = 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<CatalogueFragment>() {
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<Filter> = 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<CatalogueFragment>() {
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<List<Manga>> {
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<MangasPage>): Observable<MangasPage> {
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 <T> MutableList<T>.replace(block: (T) -> T) {
forEachIndexed { i, obj ->
set(i, block(obj))
}
}
/**
@ -299,7 +305,7 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
* @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<CatalogueFragment>() {
/**
* Returns a list of enabled sources ordered by language and name.
*/
private fun getEnabledSources(): List<OnlineSource> {
open protected fun getEnabledSources(): List<OnlineSource> {
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<CatalogueFragment>() {
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<CatalogueFragment>() {
prefs.catalogueAsList().set(!isListMode)
}
/**
* Set the active filters for the current source.
*
* @param selectedFilters a list of active filters.
*/
fun setSourceFilter(selectedFilters: List<Filter>) {
restartPager(filters = selectedFilters)
}
open fun createPager(query: String, filters: List<Filter>): Pager {
return CataloguePager(source, query, filters)
}
}

View File

@ -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<MangasPage>()
fun results(): Observable<MangasPage> {
return results.asObservable()
}
fun hasNextPage(): Boolean {
return lastPage == null || lastPage?.nextPageUrl != null
}
abstract fun requestNext(transformer: (Observable<MangasPage>) -> Observable<MangasPage>): Observable<MangasPage>
}

View File

@ -55,9 +55,9 @@ class CategoryActivity : BaseRxActivity<CategoryPresenter>(), 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)

View File

@ -35,7 +35,7 @@ class DownloadPresenter : BasePresenter<DownloadFragment>() {
.subscribeLatestCache({ view, downloads ->
view.onNextDownloads(downloads)
}, { view, error ->
Timber.e(error, error.message)
Timber.e(error)
})
}

View File

@ -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()
}
}
}

View File

@ -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<MangasPage>) -> Observable<MangasPage>): Observable<MangasPage> {
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 }
}
}

View File

@ -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<Filter>): Pager {
return LatestUpdatesPager(source)
}
override fun getEnabledSources(): List<OnlineSource> {
return super.getEnabledSources().filter { it.supportsLatest }
}
override fun isValidSource(source: Source?): Boolean {
return super.isValidSource(source) && (source as OnlineSource).supportsLatest
}
}

View File

@ -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<Category>? = null
var categories: List<Category> = 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
}
}

View File

@ -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<LibraryHolder, Manga>() {
/**
@ -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 }
}
}

View File

@ -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<Int> {
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
}

View File

@ -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()
}
}

View File

@ -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<LibraryPresenter>(), 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<LibraryPresenter>(), 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<LibraryPresenter>(), 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<LibraryPresenter>(), 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<LibraryPresenter>(), 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<LibraryPresenter>(), 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<LibraryPresenter>(), ActionMode.Callback
}
override fun onDestroyView() {
numColumnsSubscription?.unsubscribe()
tabs.setupWithViewPager(null)
tabs.visibility = View.GONE
super.onDestroyView()
@ -184,6 +214,7 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
return true
}
})
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -192,7 +223,7 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), 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<LibraryPresenter>(), 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<LibraryPresenter>(), 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<LibraryPresenter>(), 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<Int> {
return if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT)
preferences.portraitColumns()
else
preferences.landscapeColumns()
}
/**
* Updates the query.
*
@ -268,7 +328,7 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), 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<LibraryPresenter>(), 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<LibraryPresenter>(), 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<LibraryPresenter>(), 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<LibraryPresenter>(), 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()
}
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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<LibraryFragment>() {
/**
* Categories of the library.
*/
lateinit var categories: List<Category>
var categories: List<Category> = emptyList()
/**
* Currently selected manga.
*/
var selectedMangas = mutableListOf<Manga>()
val selectedMangas = mutableListOf<Manga>()
/**
* Search query of the library.
*/
val searchSubject = BehaviorSubject.create<String>()
val searchSubject: BehaviorSubject<String> = BehaviorSubject.create()
/**
* Subject to notify the library's viewpager for updates.
*/
val libraryMangaSubject = BehaviorSubject.create<LibraryMangaEvent>()
val libraryMangaSubject: BehaviorSubject<LibraryMangaEvent> = BehaviorSubject.create()
/**
* Subject to notify the UI of selection updates.
*/
val selectionSubject: PublishSubject<LibrarySelectionEvent> = PublishSubject.create()
/**
* Database.
@ -149,7 +155,7 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
/**
* Resubscribes to library.
*/
fun updateLibrary() {
fun resubscribeLibrary() {
start(GET_LIBRARY)
}
@ -219,17 +225,27 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
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<Manga>) = mangas.toSet()
fun getCommonCategories(mangas: List<Manga>): Collection<Category> = mangas.toSet()
.map { db.getCategoriesForManga(it).executeAsBlocking() }
.reduce { set1: Iterable<Category>, set2 -> set1.intersect(set2) }
@ -285,4 +301,12 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
return false
}
/**
* Changes the active display mode.
*/
fun swapDisplayMode() {
val displayAsList = preferences.libraryAsList().getOrDefault()
preferences.libraryAsList().set(!displayAsList)
}
}

View File

@ -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()
}

View File

@ -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)

View File

@ -23,6 +23,7 @@ class MangaActivity : BaseRxActivity<MangaPresenter>() {
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<MangaPresenter>() {
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()!!)

View File

@ -116,8 +116,25 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), 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<ChaptersPresenter>(), 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<ChaptersPresenter>(), ActionMode.Callbac
fun onNextManga(manga: Manga) {
// Set initial values
setReadFilter()
setDownloadedFilter()
activity.supportInvalidateOptionsMenu()
}
fun onNextChapters(chapters: List<ChapterModel>) {
@ -242,6 +264,7 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), 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<ChaptersPresenter>(), ActionMode.Callbac
fun onChaptersDeletedError(error: Throwable) {
dismissDeletingDialog()
Timber.e(error, error.message)
Timber.e(error)
}
fun dismissDeletingDialog() {
@ -394,12 +417,4 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
private fun setContextTitle(count: Int) {
actionMode?.title = getString(R.string.label_selected, count)
}
fun setReadFilter() {
activity.supportInvalidateOptionsMenu()
}
fun setDownloadedFilter() {
activity.supportInvalidateOptionsMenu()
}
}

View File

@ -111,7 +111,7 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
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<ChaptersFragment>() {
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<ChaptersFragment>() {
*
* @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<ChaptersFragment>() {
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.
*/

View File

@ -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<MangaInfoPresenter>() {
fun newInstance(): MangaInfoFragment {
return MangaInfoFragment()
}
}
override fun onCreate(savedState: Bundle?) {
@ -59,6 +74,8 @@ class MangaInfoFragment : BaseRxFragment<MangaInfoPresenter>() {
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<MangaInfoPresenter>() {
}
}
/**
* 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<out Any>) {
val modes = intArrayOf(R.string.circular_icon,
R.string.rounded_icon,
R.string.square_icon,
R.string.star_icon)
fun BitmapRequestBuilder<out Any, Bitmap>.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.
*

View File

@ -128,5 +128,4 @@ class MangaInfoPresenter : BasePresenter<MangaInfoFragment>() {
private fun refreshManga() {
start(GET_MANGA)
}
}

View File

@ -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)
}
})
}

View File

@ -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<ReaderPresenter>() {
@ -69,6 +72,8 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
private var customBrightnessSubscription: Subscription? = null
private var customFilterColorSubscription: Subscription? = null
var readerTheme: Int = 0
private set
@ -105,7 +110,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
setMenuVisibility(menuVisible)
maxBitmapSize = GLUtil.getMaxTextureSize()
maxBitmapSize = Math.min(2048, GLUtil.getMaxTextureSize())
left_chapter.setOnClickListener {
if (viewer != null) {
@ -139,6 +144,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
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<ReaderPresenter>() {
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<ReaderPresenter>() {
}
fun onChapterError(error: Throwable) {
Timber.e(error, error.message)
Timber.e(error)
finish()
toast(error.message)
}
@ -301,15 +314,18 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
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<ReaderPresenter>() {
val requestedPage = activePage.chapter.pages!![pageIndex]
it.setActivePage(requestedPage)
}
}
}
@ -344,9 +359,9 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
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<ReaderPresenter>() {
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<ReaderPresenter>() {
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<ReaderPresenter>() {
}
}
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<ReaderPresenter>() {
}
}
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<ReaderPresenter>() {
}
}
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)
}
}
}

View File

@ -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<PreferencesHelper>()
/**
* 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<Int> {
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()
}
}

View File

@ -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<ReaderActivity>() {
* 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<ReaderActivity>() {
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<ReaderActivity>() {
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()
}
/**

View File

@ -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() {

View File

@ -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()
}
/**

View File

@ -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<Int>())
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<Int>())
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
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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)
}
}

View File

@ -253,7 +253,7 @@ class RecentChaptersFragment
*/
fun onChaptersDeletedError(error: Throwable) {
dismissDeletingDialog()
Timber.e(error, error.message)
Timber.e(error)
}
/**

View File

@ -71,7 +71,7 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
// Set chapter status
view.onChapterStatusChange(download)
},
{ view, error -> Timber.e(error.cause, error.message) }
{ view, error -> Timber.e(error) }
)
if (savedState == null) {

View File

@ -115,7 +115,7 @@ class RecentlyReadPresenter : BasePresenter<RecentlyReadFragment>() {
.subscribeFirst({ view, chapter ->
view.onOpenNextChapter(chapter, manga)
}, { view, error ->
Timber.e(error, error.message)
Timber.e(error)
})
}

View File

@ -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)
})
}

View File

@ -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() {

View File

@ -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)

View File

@ -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 <reified T : Preference> bindPref(resId: Int): Lazy<T> {
return lazy { findPreference(getString(resId)) as T }
}
}

View File

@ -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()

View File

@ -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<SourceManager>().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"
}
}

View File

@ -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()
}
}
}

View File

@ -1,21 +0,0 @@
package eu.kanade.tachiyomi.util
import android.util.Pair
import rx.Observable
import rx.subjects.PublishSubject
class RxPager<T> {
private val results = PublishSubject.create<List<T>>()
private var requestedCount: Int = 0
fun results(): Observable<Pair<Int, List<T>>> {
requestedCount = 0
return results.map { Pair(requestedCount++, it) }
}
fun request(networkObservable: (Int) -> Observable<List<T>>) =
networkObservable(requestedCount).doOnNext { results.onNext(it) }
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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<View>()
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
}
}

View File

@ -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) {
}
}

View File

@ -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
}
}

View File

@ -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()
}
}

View File

@ -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)
})
}
}
}

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,5c1.66,0 3,1.34 3,3s-1.34,3 -3,3 -3,-1.34 -3,-3 1.34,-3 3,-3zM12,19.2c-2.5,0 -4.71,-1.28 -6,-3.22 0.03,-1.99 4,-3.08 6,-3.08 1.99,0 5.97,1.09 6,3.08 -1.29,1.94 -3.5,3.22 -6,3.22z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M20,8.69V4h-4.69L12,0.69 8.69,4H4v4.69L0.69,12 4,15.31V20h4.69L12,23.31 15.31,20H20v-4.69L23.31,12 20,8.69zM12,18c-0.89,0 -1.74,-0.2 -2.5,-0.55C11.56,16.5 13,14.42 13,12s-1.44,-4.5 -3.5,-5.45C10.26,6.2 11.11,6 12,6c3.31,0 6,2.69 6,6s-2.69,6 -6,6z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M20,15.31L23.31,12 20,8.69V4h-4.69L12,0.69 8.69,4H4v4.69L0.69,12 4,15.31V20h4.69L12,23.31 15.31,20H20v-4.69zM12,18c-3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6 6,2.69 6,6 -2.69,6 -6,6z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#ffffff"
android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
</vector>

View File

@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#ffffff"
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"/>
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" />
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,2C6.5,2 2,6.5 2,12s4.5,10 10,10 10,-4.5 10,-10S17.5,2 12,2zM16.2,16.2L11,13L11,7h1.5v5.2l4.5,2.7 -0.8,1.3z"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 B

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