Merge changes.
Various changes.
This commit is contained in:
commit
3cafbd9141
@ -5,13 +5,16 @@ android:
|
|||||||
- tools
|
- tools
|
||||||
|
|
||||||
# The BuildTools version used by your project
|
# The BuildTools version used by your project
|
||||||
- build-tools-23.0.3
|
- build-tools-24.0.2
|
||||||
- android-23
|
- android-24
|
||||||
- extra-android-m2repository
|
- extra-android-m2repository
|
||||||
- extra-google-m2repository
|
- extra-google-m2repository
|
||||||
- extra-android-support
|
- extra-android-support
|
||||||
- extra-google-google_play_services
|
- extra-google-google_play_services
|
||||||
|
|
||||||
|
jdk:
|
||||||
|
- oraclejdk8
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- chmod +x gradlew
|
- chmod +x gradlew
|
||||||
#Build, and run tests
|
#Build, and run tests
|
||||||
|
0
CHANGES.md
Normal file
0
CHANGES.md
Normal file
3
app/.gitignore
vendored
3
app/.gitignore
vendored
@ -1,4 +1,5 @@
|
|||||||
/build
|
/build
|
||||||
*iml
|
*iml
|
||||||
*.iml
|
*.iml
|
||||||
.idea
|
custom.gradle
|
||||||
|
google-services.json
|
@ -4,6 +4,10 @@ apply plugin: 'com.android.application'
|
|||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'kotlin-android-extensions'
|
apply plugin: 'kotlin-android-extensions'
|
||||||
|
|
||||||
|
if (file("custom.gradle").exists()) {
|
||||||
|
apply from: "custom.gradle"
|
||||||
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
// Git is needed in your system PATH for these commands to work.
|
// 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
|
// If it's not installed, you can return a random value as a workaround
|
||||||
@ -29,14 +33,14 @@ def includeUpdater() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 23
|
compileSdkVersion 24
|
||||||
buildToolsVersion "23.0.3"
|
buildToolsVersion "24.0.2"
|
||||||
publishNonDefault true
|
publishNonDefault true
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "eu.kanade.tachiyomi.eh"
|
applicationId "eu.kanade.tachiyomi.eh"
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 23
|
targetSdkVersion 24
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
versionCode 2180
|
versionCode 2180
|
||||||
versionName "v2.18.0-EH"
|
versionName "v2.18.0-EH"
|
||||||
@ -47,17 +51,22 @@ android {
|
|||||||
buildConfigField "boolean", "INCLUDE_UPDATER", "${includeUpdater()}"
|
buildConfigField "boolean", "INCLUDE_UPDATER", "${includeUpdater()}"
|
||||||
|
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|
||||||
|
ndk {
|
||||||
|
abiFilters "armeabi", "armeabi-v7a", "x86"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
minifyEnabled false
|
versionNameSuffix "-${getCommitCount()}"
|
||||||
shrinkResources true
|
applicationIdSuffix ".debug"
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
multiDexEnabled true
|
||||||
}
|
}
|
||||||
release {
|
release {
|
||||||
minifyEnabled false
|
minifyEnabled true
|
||||||
shrinkResources true
|
shrinkResources true
|
||||||
|
multiDexEnabled false
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -84,11 +93,11 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
// Modified 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'
|
compile 'com.github.inorichi:ReactiveNetwork:69092ed'
|
||||||
|
|
||||||
// Android support library
|
// 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:support-v4:$support_library_version"
|
||||||
compile "com.android.support:appcompat-v7:$support_library_version"
|
compile "com.android.support:appcompat-v7:$support_library_version"
|
||||||
compile "com.android.support:cardview-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:support-annotations:$support_library_version"
|
||||||
compile "com.android.support:customtabs:$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
|
// ReactiveX
|
||||||
compile 'io.reactivex:rxandroid:1.2.1'
|
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'
|
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
|
||||||
|
|
||||||
// Network client
|
// Network client
|
||||||
compile "com.squareup.okhttp3:okhttp:3.3.1"
|
compile "com.squareup.okhttp3:okhttp:3.4.1"
|
||||||
|
|
||||||
// REST
|
// REST
|
||||||
final retrofit_version = '2.1.0'
|
final retrofit_version = '2.1.0'
|
||||||
@ -112,17 +125,17 @@ dependencies {
|
|||||||
compile "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
|
compile "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
|
||||||
|
|
||||||
// IO
|
// IO
|
||||||
compile 'com.squareup.okio:okio:1.8.0'
|
compile 'com.squareup.okio:okio:1.10.0'
|
||||||
|
|
||||||
// JSON
|
// JSON
|
||||||
compile 'com.google.code.gson:gson:2.7'
|
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
|
// YAML
|
||||||
compile 'com.github.bmoliveira:snake-yaml:v1.18-android'
|
compile 'com.github.bmoliveira:snake-yaml:v1.18-android'
|
||||||
|
|
||||||
// JavaScript engine
|
// JavaScript engine
|
||||||
compile 'com.squareup.duktape:duktape-android:0.9.5'
|
compile 'com.squareup.duktape:duktape-android:1.0.0'
|
||||||
|
|
||||||
// Disk cache
|
// Disk cache
|
||||||
compile 'com.jakewharton:disklrucache:2.0.2'
|
compile 'com.jakewharton:disklrucache:2.0.2'
|
||||||
@ -134,7 +147,7 @@ dependencies {
|
|||||||
compile 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
|
compile 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
compile "com.pushtorefresh.storio:sqlite:1.9.0"
|
compile "com.pushtorefresh.storio:sqlite:1.11.0"
|
||||||
|
|
||||||
// Model View Presenter
|
// Model View Presenter
|
||||||
final nucleus_version = '3.0.0'
|
final nucleus_version = '3.0.0'
|
||||||
@ -148,23 +161,25 @@ dependencies {
|
|||||||
// Image library
|
// Image library
|
||||||
compile 'com.github.bumptech.glide:glide:3.7.0'
|
compile 'com.github.bumptech.glide:glide:3.7.0'
|
||||||
compile 'com.github.bumptech.glide:okhttp3-integration:1.4.0@aar'
|
compile 'com.github.bumptech.glide:okhttp3-integration:1.4.0@aar'
|
||||||
|
// Transformations
|
||||||
|
compile 'jp.wasabeef:glide-transformations:2.0.1'
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
compile 'com.jakewharton.timber:timber:4.1.2'
|
compile 'com.jakewharton.timber:timber:4.3.1'
|
||||||
|
|
||||||
// Crash reports
|
// Crash reports
|
||||||
compile 'ch.acra:acra:4.9.0'
|
compile 'ch.acra:acra:4.9.1'
|
||||||
|
|
||||||
// UI
|
// 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 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
|
||||||
compile 'eu.davidea:flexible-adapter:4.2.0'
|
compile 'eu.davidea:flexible-adapter:4.2.0'
|
||||||
compile 'com.nononsenseapps:filepicker:2.5.2'
|
compile 'com.nononsenseapps:filepicker:2.5.2'
|
||||||
compile 'com.github.amulyakhare:TextDrawable:558677e'
|
compile 'com.github.amulyakhare:TextDrawable:558677e'
|
||||||
compile 'com.afollestad.material-dialogs:core:0.8.6.1'
|
compile 'com.afollestad.material-dialogs:core:0.9.0.2'
|
||||||
compile 'net.xpece.android:support-preference:0.8.1'
|
compile 'net.xpece.android:support-preference:1.0.3'
|
||||||
compile 'me.zhanghai.android.systemuihelper:library:1.0.0'
|
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
|
//EXH
|
||||||
compile 'com.jakewharton:process-phoenix:1.0.2'
|
compile 'com.jakewharton:process-phoenix:1.0.2'
|
||||||
@ -173,13 +188,15 @@ dependencies {
|
|||||||
testCompile 'junit:junit:4.12'
|
testCompile 'junit:junit:4.12'
|
||||||
testCompile 'org.assertj:assertj-core:1.7.1'
|
testCompile 'org.assertj:assertj-core:1.7.1'
|
||||||
testCompile 'org.mockito:mockito-core:1.10.19'
|
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"
|
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.0.3'
|
ext.kotlin_version = '1.0.4'
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
2
app/proguard-rules.pro
vendored
2
app/proguard-rules.pro
vendored
@ -1,5 +1,7 @@
|
|||||||
-dontobfuscate
|
-dontobfuscate
|
||||||
|
|
||||||
|
-keep class eu.kanade.tachiyomi.**
|
||||||
|
|
||||||
# OkHttp
|
# OkHttp
|
||||||
-keepattributes Signature
|
-keepattributes Signature
|
||||||
-keepattributes *Annotation*
|
-keepattributes *Annotation*
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="eu.kanade.tachiyomi">
|
package="eu.kanade.tachiyomi">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
@ -8,6 +10,8 @@
|
|||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
<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
|
<application
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
@ -18,8 +22,7 @@
|
|||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:theme="@style/Theme.Tachiyomi" >
|
android:theme="@style/Theme.Tachiyomi" >
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.main.MainActivity"
|
android:name=".ui.main.MainActivity">
|
||||||
android:theme="@style/Theme.BrandedLaunch">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
|
||||||
@ -28,7 +31,8 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.manga.MangaActivity"
|
android:name=".ui.manga.MangaActivity"
|
||||||
android:parentActivityName=".ui.main.MainActivity">
|
android:parentActivityName=".ui.main.MainActivity"
|
||||||
|
android:exported="true">
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.reader.ReaderActivity"
|
android:name=".ui.reader.ReaderActivity"
|
||||||
@ -59,47 +63,33 @@
|
|||||||
<service android:name=".data.mangasync.UpdateMangaSyncService"
|
<service android:name=".data.mangasync.UpdateMangaSyncService"
|
||||||
android:exported="false"/>
|
android:exported="false"/>
|
||||||
|
|
||||||
<receiver
|
<service
|
||||||
android:name=".data.library.LibraryUpdateService$SyncOnConnectionAvailable"
|
android:name=".data.library.LibraryUpdateTrigger"
|
||||||
android:enabled="false">
|
android:exported="true"
|
||||||
|
android:permission="com.google.android.gms.permission.BIND_NETWORK_TASK_SERVICE">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
|
<action android:name="com.google.android.gms.gcm.ACTION_TASK_READY" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</service>
|
||||||
|
|
||||||
<receiver
|
<service
|
||||||
android:name=".data.library.LibraryUpdateService$SyncOnPowerConnected"
|
android:name=".data.updater.UpdateCheckerService"
|
||||||
android:enabled="false">
|
android:exported="true"
|
||||||
|
android:permission="com.google.android.gms.permission.BIND_NETWORK_TASK_SERVICE">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.ACTION_POWER_CONNECTED" />
|
<action android:name="com.google.android.gms.gcm.ACTION_TASK_READY" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</service>
|
||||||
|
|
||||||
|
<service android:name=".data.updater.UpdateDownloaderService"
|
||||||
|
android:exported="false"/>
|
||||||
|
|
||||||
|
<receiver android:name=".data.updater.UpdateNotificationReceiver"/>
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".data.library.LibraryUpdateService$CancelUpdateReceiver">
|
android:name=".data.library.LibraryUpdateService$CancelUpdateReceiver">
|
||||||
</receiver>
|
</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
|
<meta-data
|
||||||
android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"
|
android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"
|
||||||
android:value="GlideModule" />
|
android:value="GlideModule" />
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi
|
package eu.kanade.tachiyomi
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.support.multidex.MultiDex
|
||||||
import org.acra.ACRA
|
import org.acra.ACRA
|
||||||
import org.acra.annotation.ReportsCrashes
|
import org.acra.annotation.ReportsCrashes
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@ -27,6 +29,13 @@ open class App : Application() {
|
|||||||
setupAcra()
|
setupAcra()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun attachBaseContext(base: Context) {
|
||||||
|
super.attachBaseContext(base)
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
MultiDex.install(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected open fun setupAcra() {
|
protected open fun setupAcra() {
|
||||||
ACRA.init(this)
|
ACRA.init(this)
|
||||||
}
|
}
|
||||||
|
@ -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()
|
|
||||||
|
|
||||||
}
|
|
@ -80,10 +80,10 @@ class DownloadManager(
|
|||||||
if (areAllDownloadsFinished()) {
|
if (areAllDownloadsFinished()) {
|
||||||
DownloadService.stop(context)
|
DownloadService.stop(context)
|
||||||
}
|
}
|
||||||
}, { e ->
|
}, { error ->
|
||||||
DownloadService.stop(context)
|
DownloadService.stop(context)
|
||||||
Timber.e(e, e.message)
|
Timber.e(error)
|
||||||
downloadNotifier.onError(e.message)
|
downloadNotifier.onError(error.message)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!isRunning) {
|
if (!isRunning) {
|
||||||
@ -369,8 +369,8 @@ class DownloadManager(
|
|||||||
try {
|
try {
|
||||||
it.write(gson.toJson(pages).toByteArray())
|
it.write(gson.toJson(pages).toByteArray())
|
||||||
it.flush()
|
it.flush()
|
||||||
} catch (e: Exception) {
|
} catch (error: Exception) {
|
||||||
Timber.e(e, e.message)
|
Timber.e(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -96,6 +96,10 @@ class DownloadNotifier(private val context: Context) {
|
|||||||
if (multipleDownloadThreads) {
|
if (multipleDownloadThreads) {
|
||||||
setContentTitle(context.getString(R.string.app_name))
|
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)
|
setContentText(context.getString(R.string.chapter_downloading_progress)
|
||||||
.format(initialQueueSize - queue.size, initialQueueSize))
|
.format(initialQueueSize - queue.size, initialQueueSize))
|
||||||
setProgress(initialQueueSize, initialQueueSize - queue.size, false)
|
setProgress(initialQueueSize, initialQueueSize - queue.size, false)
|
||||||
@ -161,6 +165,9 @@ class DownloadNotifier(private val context: Context) {
|
|||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
}
|
}
|
||||||
context.notificationManager.notify(Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID, notificationBuilder.build())
|
context.notificationManager.notify(Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID, notificationBuilder.build())
|
||||||
|
|
||||||
|
// Reset download information
|
||||||
|
onClear()
|
||||||
isDownloading = false
|
isDownloading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -5,11 +5,11 @@ import android.app.Service
|
|||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.support.v4.app.NotificationCompat
|
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.Constants
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
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.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
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.SourceManager
|
||||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
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.Observable
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
@ -69,18 +73,20 @@ class LibraryUpdateService : Service() {
|
|||||||
private val notificationId: Int
|
private val notificationId: Int
|
||||||
get() = Constants.NOTIFICATION_LIBRARY_ID
|
get() = Constants.NOTIFICATION_LIBRARY_ID
|
||||||
|
|
||||||
|
private var notificationBitmap: Bitmap? = null
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
|
||||||
* Key for manual library update.
|
|
||||||
*/
|
|
||||||
const val UPDATE_IS_MANUAL = "is_manual"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Key for category to update.
|
* Key for category to update.
|
||||||
*/
|
*/
|
||||||
const val UPDATE_CATEGORY = "category"
|
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.
|
* Returns the status of the service.
|
||||||
*
|
*
|
||||||
@ -96,13 +102,13 @@ class LibraryUpdateService : Service() {
|
|||||||
* running.
|
* running.
|
||||||
*
|
*
|
||||||
* @param context the application context.
|
* @param context the application context.
|
||||||
* @param isManual whether the update has been manually triggered.
|
* @param category a specific category to update, or null for global update.
|
||||||
* @param category a specific category to update, or null for all in the library.
|
* @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)) {
|
if (!isRunning(context)) {
|
||||||
val intent = Intent(context, LibraryUpdateService::class.java).apply {
|
val intent = Intent(context, LibraryUpdateService::class.java).apply {
|
||||||
putExtra(UPDATE_IS_MANUAL, isManual)
|
putExtra(UPDATE_DETAILS, details)
|
||||||
category?.let { putExtra(UPDATE_CATEGORY, it.id) }
|
category?.let { putExtra(UPDATE_CATEGORY, it.id) }
|
||||||
}
|
}
|
||||||
context.startService(intent)
|
context.startService(intent)
|
||||||
@ -135,7 +141,8 @@ class LibraryUpdateService : Service() {
|
|||||||
*/
|
*/
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
subscription?.unsubscribe()
|
subscription?.unsubscribe()
|
||||||
LibraryUpdateAlarm.startAlarm(this)
|
notificationBitmap?.recycle()
|
||||||
|
notificationBitmap = null
|
||||||
destroyWakeLock()
|
destroyWakeLock()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
@ -156,61 +163,36 @@ class LibraryUpdateService : Service() {
|
|||||||
* @return the start value of the command.
|
* @return the start value of the command.
|
||||||
*/
|
*/
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
if (intent == null) return Service.START_NOT_STICKY
|
||||||
// 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)
|
|
||||||
|
|
||||||
// Unsubscribe from any previous subscription if needed.
|
// Unsubscribe from any previous subscription if needed.
|
||||||
subscription?.unsubscribe()
|
subscription?.unsubscribe()
|
||||||
|
|
||||||
// Update favorite manga. Destroy service when completed or in case of an error.
|
// Update favorite manga. Destroy service when completed or in case of an error.
|
||||||
subscription = Observable.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())
|
.subscribeOn(Schedulers.io())
|
||||||
.subscribe({},
|
.subscribe({
|
||||||
{
|
}, {
|
||||||
showNotification(getString(R.string.notification_update_error), "")
|
showNotification(getString(R.string.notification_update_error), "")
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
}, {
|
}, {
|
||||||
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.
|
* @param intent the update intent.
|
||||||
* @return a list of manga to update
|
* @return a list of manga to update
|
||||||
*/
|
*/
|
||||||
fun getMangaToUpdate(intent: Intent?): List<Manga> {
|
fun getMangaToUpdate(intent: Intent): List<Manga> {
|
||||||
val categoryId = intent?.getIntExtra(UPDATE_CATEGORY, -1) ?: -1
|
val categoryId = intent.getIntExtra(UPDATE_CATEGORY, -1)
|
||||||
|
|
||||||
var toUpdate = if (categoryId != -1)
|
var listToUpdate = if (categoryId != -1)
|
||||||
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
|
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
|
||||||
|
else {
|
||||||
|
val categoriesToUpdate = preferences.libraryUpdateCategories().getOrDefault().map { it.toInt() }
|
||||||
|
if (categoriesToUpdate.isNotEmpty())
|
||||||
|
db.getLibraryMangas().executeAsBlocking()
|
||||||
|
.filter { it.category in categoriesToUpdate }
|
||||||
|
.distinctBy { it.id }
|
||||||
else
|
else
|
||||||
db.getFavoriteMangas().executeAsBlocking()
|
db.getFavoriteMangas().executeAsBlocking().distinctBy { it.id }
|
||||||
|
|
||||||
if (preferences.updateOnlyNonCompleted()) {
|
|
||||||
toUpdate = toUpdate.filter { it.status != Manga.COMPLETED }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
* @param mangaToUpdate the list to update
|
||||||
* @return an observable delivering the progress of each 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.
|
// Initialize the variables holding the progress of the updates.
|
||||||
val count = AtomicInteger(0)
|
val count = AtomicInteger(0)
|
||||||
val newUpdates = ArrayList<Manga>()
|
val newUpdates = ArrayList<Manga>()
|
||||||
@ -278,6 +267,7 @@ class LibraryUpdateService : Service() {
|
|||||||
} else {
|
} else {
|
||||||
showResultNotification(newUpdates, failedUpdates)
|
showResultNotification(newUpdates, failedUpdates)
|
||||||
}
|
}
|
||||||
|
LibraryUpdateTrigger.setupTask(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -293,6 +283,43 @@ class LibraryUpdateService : Service() {
|
|||||||
.map { syncChaptersWithSource(db, it, manga, source) }
|
.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.
|
* 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) {
|
private fun showNotification(title: String, body: String) {
|
||||||
notificationManager.notify(notificationId, notification() {
|
notificationManager.notify(notificationId, notification() {
|
||||||
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
||||||
|
setLargeIcon(notificationBitmap)
|
||||||
setContentTitle(title)
|
setContentTitle(title)
|
||||||
setContentText(body)
|
setContentText(body)
|
||||||
})
|
})
|
||||||
@ -366,6 +394,7 @@ class LibraryUpdateService : Service() {
|
|||||||
private fun showProgressNotification(manga: Manga, current: Int, total: Int, cancelIntent: PendingIntent) {
|
private fun showProgressNotification(manga: Manga, current: Int, total: Int, cancelIntent: PendingIntent) {
|
||||||
notificationManager.notify(notificationId, notification() {
|
notificationManager.notify(notificationId, notification() {
|
||||||
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
||||||
|
setLargeIcon(notificationBitmap)
|
||||||
setContentTitle(manga.title)
|
setContentTitle(manga.title)
|
||||||
setProgress(total, current, false)
|
setProgress(total, current, false)
|
||||||
setOngoing(true)
|
setOngoing(true)
|
||||||
@ -386,6 +415,7 @@ class LibraryUpdateService : Service() {
|
|||||||
|
|
||||||
notificationManager.notify(notificationId, notification() {
|
notificationManager.notify(notificationId, notification() {
|
||||||
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
||||||
|
setLargeIcon(notificationBitmap)
|
||||||
setContentTitle(title)
|
setContentTitle(title)
|
||||||
setStyle(NotificationCompat.BigTextStyle().bigText(body))
|
setStyle(NotificationCompat.BigTextStyle().bigText(body))
|
||||||
setContentIntent(notificationIntent)
|
setContentIntent(notificationIntent)
|
||||||
@ -410,41 +440,6 @@ class LibraryUpdateService : Service() {
|
|||||||
return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
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.
|
* Class that stops updating the library.
|
||||||
*/
|
*/
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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 }
|
|
||||||
|
|
||||||
}
|
|
@ -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)
|
|
||||||
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -60,8 +60,7 @@ class CloudflareInterceptor(private val cookies: PersistentCookieStore) : Interc
|
|||||||
.replace(Regex("""\s{3,}[a-z](?: = |\.).+"""), "")
|
.replace(Regex("""\s{3,}[a-z](?: = |\.).+"""), "")
|
||||||
.replace("\n", "")
|
.replace("\n", "")
|
||||||
|
|
||||||
// Duktape can only return strings, so the result has to be converted to string first
|
val result = (duktape.evaluate(js) as Double).toInt()
|
||||||
val result = duktape.evaluate("$js.toString()").toInt()
|
|
||||||
|
|
||||||
val answer = "${result + domain.length}"
|
val answer = "${result + domain.length}"
|
||||||
|
|
||||||
|
@ -26,6 +26,10 @@ class PreferenceKeys(context: Context) {
|
|||||||
|
|
||||||
val customBrightnessValue = context.getString(R.string.pref_custom_brightness_value_key)
|
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 defaultViewer = context.getString(R.string.pref_default_viewer_key)
|
||||||
|
|
||||||
val imageScaleType = context.getString(R.string.pref_image_scale_type_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 downloadOnlyOverWifi = context.getString(R.string.pref_download_only_over_wifi_key)
|
||||||
|
|
||||||
val removeAfterRead = context.getString(R.string.pref_remove_after_read_key)
|
val removeAfterReadSlots = context.getString(R.string.pref_remove_after_read_slots_key)
|
||||||
|
|
||||||
val removeAfterReadPrevious = context.getString(R.string.pref_remove_after_read_previous_key)
|
|
||||||
|
|
||||||
val removeAfterMarkedAsRead = context.getString(R.string.pref_remove_after_marked_as_read_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 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 filterDownloaded = context.getString(R.string.pref_filter_downloaded_key)
|
||||||
|
|
||||||
val filterUnread = context.getString(R.string.pref_filter_unread_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)
|
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"
|
fun syncPassword(syncId: Int) = "pref_mangasync_password_$syncId"
|
||||||
|
|
||||||
|
val libraryAsList = context.getString(R.string.pref_display_library_as_list)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -52,6 +52,10 @@ class PreferencesHelper(context: Context) {
|
|||||||
|
|
||||||
fun customBrightnessValue() = rxPrefs.getInteger(keys.customBrightnessValue, 0)
|
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 defaultViewer() = prefs.getInt(keys.defaultViewer, 1)
|
||||||
|
|
||||||
fun imageScaleType() = rxPrefs.getInteger(keys.imageScaleType, 1)
|
fun imageScaleType() = rxPrefs.getInteger(keys.imageScaleType, 1)
|
||||||
@ -116,9 +120,7 @@ class PreferencesHelper(context: Context) {
|
|||||||
|
|
||||||
fun downloadOnlyOverWifi() = prefs.getBoolean(keys.downloadOnlyOverWifi, true)
|
fun downloadOnlyOverWifi() = prefs.getBoolean(keys.downloadOnlyOverWifi, true)
|
||||||
|
|
||||||
fun removeAfterRead() = prefs.getBoolean(keys.removeAfterRead, false)
|
fun removeAfterReadSlots() = prefs.getInt(keys.removeAfterReadSlots, -1)
|
||||||
|
|
||||||
fun removeAfterReadPrevious() = prefs.getBoolean(keys.removeAfterReadPrevious, false)
|
|
||||||
|
|
||||||
fun removeAfterMarkedAsRead() = prefs.getBoolean(keys.removeAfterMarkedAsRead, false)
|
fun removeAfterMarkedAsRead() = prefs.getBoolean(keys.removeAfterMarkedAsRead, false)
|
||||||
|
|
||||||
@ -126,10 +128,16 @@ class PreferencesHelper(context: Context) {
|
|||||||
|
|
||||||
fun libraryUpdateRestriction() = prefs.getStringSet(keys.libraryUpdateRestriction, emptySet())
|
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 filterDownloaded() = rxPrefs.getBoolean(keys.filterDownloaded, false)
|
||||||
|
|
||||||
fun filterUnread() = rxPrefs.getBoolean(keys.filterUnread, 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())
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -47,5 +47,4 @@ interface Source {
|
|||||||
* @param page the page.
|
* @param page the page.
|
||||||
*/
|
*/
|
||||||
fun fetchImage(page: Page): Observable<Page>
|
fun fetchImage(page: Page): Observable<Page>
|
||||||
|
|
||||||
}
|
}
|
@ -15,17 +15,7 @@ import java.io.File
|
|||||||
|
|
||||||
open class SourceManager(private val context: Context) {
|
open class SourceManager(private val context: Context) {
|
||||||
|
|
||||||
val EHENTAI = 1
|
private val sourcesMap = createSources()
|
||||||
val EXHENTAI = 2
|
|
||||||
|
|
||||||
val LAST_SOURCE by lazy {
|
|
||||||
if (DialogLogin.isLoggedIn(context, false))
|
|
||||||
2
|
|
||||||
else
|
|
||||||
1
|
|
||||||
}
|
|
||||||
|
|
||||||
val sourcesMap = createSources()
|
|
||||||
|
|
||||||
open fun get(sourceKey: Int): Source? {
|
open fun get(sourceKey: Int): Source? {
|
||||||
return sourcesMap[sourceKey]
|
return sourcesMap[sourceKey]
|
||||||
@ -33,16 +23,14 @@ open class SourceManager(private val context: Context) {
|
|||||||
|
|
||||||
fun getOnlineSources() = sourcesMap.values.filterIsInstance(OnlineSource::class.java)
|
fun getOnlineSources() = sourcesMap.values.filterIsInstance(OnlineSource::class.java)
|
||||||
|
|
||||||
private fun createSource(id: Int): Source? = when (id) {
|
private fun createOnlineSourceList(): List<Source> =
|
||||||
EHENTAI -> EHentai(context, id, false)
|
if (DialogLogin.isLoggedIn(context, false))
|
||||||
EXHENTAI -> EHentai(context, id, true)
|
listOf(EHentai(1, false), EHentai(2, true))
|
||||||
else -> null
|
else
|
||||||
}
|
listOf(EHentai(1, false))
|
||||||
|
|
||||||
private fun createSources(): Map<Int, Source> = hashMapOf<Int, Source>().apply {
|
private fun createSources(): Map<Int, Source> = hashMapOf<Int, Source>().apply {
|
||||||
for (i in 1..LAST_SOURCE) {
|
createOnlineSourceList().forEach { put(it.id, it) }
|
||||||
createSource(i)?.let { put(i, it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
val parsersDir = File(Environment.getExternalStorageDirectory().absolutePath +
|
val parsersDir = File(Environment.getExternalStorageDirectory().absolutePath +
|
||||||
File.separator + context.getString(R.string.app_name), "parsers")
|
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" }) {
|
for (file in parsersDir.listFiles().filter { it.extension == "yml" }) {
|
||||||
try {
|
try {
|
||||||
val map = file.inputStream().use { yaml.loadAs(it, Map::class.java) }
|
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) {
|
} catch (e: Exception) {
|
||||||
Timber.e("Error loading source from file. Bad format?")
|
Timber.e("Error loading source from file. Bad format?")
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online
|
package eu.kanade.tachiyomi.data.source.online
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
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.
|
* 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.
|
* Network service.
|
||||||
@ -53,11 +50,21 @@ abstract class OnlineSource(context: Context) : Source {
|
|||||||
*/
|
*/
|
||||||
abstract val lang: Language
|
abstract val lang: Language
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the source has support for latest updates.
|
||||||
|
*/
|
||||||
|
abstract val supportsLatest : Boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Headers used for requests.
|
* Headers used for requests.
|
||||||
*/
|
*/
|
||||||
val headers by lazy { headersBuilder().build() }
|
val headers by lazy { headersBuilder().build() }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Genre filters.
|
||||||
|
*/
|
||||||
|
val filters by lazy { getFilterList() }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default network client for doing requests.
|
* Default network client for doing requests.
|
||||||
*/
|
*/
|
||||||
@ -126,11 +133,11 @@ abstract class OnlineSource(context: Context) : Source {
|
|||||||
* the current page and the next page url.
|
* the current page and the next page url.
|
||||||
* @param query the search query.
|
* @param query the search query.
|
||||||
*/
|
*/
|
||||||
open fun fetchSearchManga(page: MangasPage, query: String): Observable<MangasPage> = client
|
open fun fetchSearchManga(page: MangasPage, query: String, filters: List<Filter>): Observable<MangasPage> = client
|
||||||
.newCall(searchMangaRequest(page, query))
|
.newCall(searchMangaRequest(page, query, filters))
|
||||||
.asObservable()
|
.asObservable()
|
||||||
.map { response ->
|
.map { response ->
|
||||||
searchMangaParse(response, page, query)
|
searchMangaParse(response, page, query, filters)
|
||||||
page
|
page
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,9 +148,9 @@ abstract class OnlineSource(context: Context) : Source {
|
|||||||
* @param page the page object.
|
* @param page the page object.
|
||||||
* @param query the search query.
|
* @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) {
|
if (page.page == 1) {
|
||||||
page.url = searchMangaInitialUrl(query)
|
page.url = searchMangaInitialUrl(query, filters)
|
||||||
}
|
}
|
||||||
return GET(page.url, headers)
|
return GET(page.url, headers)
|
||||||
}
|
}
|
||||||
@ -153,7 +160,7 @@ abstract class OnlineSource(context: Context) : Source {
|
|||||||
*
|
*
|
||||||
* @param query the search query.
|
* @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
|
* 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 page the page object to be filled.
|
||||||
* @param query the search query.
|
* @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
|
* 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.
|
* @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)
|
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()
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online
|
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.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
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.
|
* 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].
|
* 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 page the page object to be filled.
|
||||||
* @param query the search query.
|
* @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()
|
val document = response.asJsoup()
|
||||||
for (element in document.select(searchMangaSelector())) {
|
for (element in document.select(searchMangaSelector())) {
|
||||||
Manga.create(id).apply {
|
Manga.create(id).apply {
|
||||||
@ -98,6 +95,38 @@ abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) {
|
|||||||
*/
|
*/
|
||||||
abstract protected fun searchMangaNextPageSelector(): String?
|
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].
|
* 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.
|
* @param document the parsed document.
|
||||||
*/
|
*/
|
||||||
abstract protected fun imageUrlParse(document: Document): String
|
abstract protected fun imageUrlParse(document: Document): String
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online
|
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.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.network.GET
|
import eu.kanade.tachiyomi.data.network.GET
|
||||||
@ -17,7 +16,7 @@ import org.jsoup.nodes.Element
|
|||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(context) {
|
class YamlOnlineSource(mappings: Map<*, *>) : OnlineSource() {
|
||||||
|
|
||||||
val map = YamlSourceNode(mappings)
|
val map = YamlSourceNode(mappings)
|
||||||
|
|
||||||
@ -32,6 +31,8 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con
|
|||||||
getLanguages().find { code == it.code }!!
|
getLanguages().find { code == it.code }!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override val supportsLatest = map.latestupdates != null
|
||||||
|
|
||||||
override val client = when(map.client) {
|
override val client = when(map.client) {
|
||||||
"cloudflare" -> network.cloudflareClient
|
"cloudflare" -> network.cloudflareClient
|
||||||
else -> network.client
|
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) {
|
if (page.page == 1) {
|
||||||
page.url = searchMangaInitialUrl(query)
|
page.url = searchMangaInitialUrl(query, filters)
|
||||||
}
|
}
|
||||||
return when (map.search.method?.toLowerCase()) {
|
return when (map.search.method?.toLowerCase()) {
|
||||||
"post" -> POST(page.url, headers, map.search.createForm())
|
"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()
|
val document = response.asJsoup()
|
||||||
for (element in document.select(map.search.manga_css)) {
|
for (element in document.select(map.search.manga_css)) {
|
||||||
Manga.create(id).apply {
|
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) {
|
override fun mangaDetailsParse(response: Response, manga: Manga) {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
with(map.manga) {
|
with(map.manga) {
|
||||||
@ -184,5 +212,4 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con
|
|||||||
throw Exception("image_regex and image_css are null")
|
throw Exception("image_regex and image_css are null")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,8 @@ class YamlSourceNode(uncheckedMap: Map<*, *>) {
|
|||||||
|
|
||||||
val popular = PopularNode(toMap(map["popular"])!!)
|
val popular = PopularNode(toMap(map["popular"])!!)
|
||||||
|
|
||||||
|
val latestupdates = toMap(map["latest_updates"])?.let { LatestUpdatesNode(it) }
|
||||||
|
|
||||||
val search = SearchNode(toMap(map["search"])!!)
|
val search = SearchNode(toMap(map["search"])!!)
|
||||||
|
|
||||||
val manga = MangaNode(toMap(map["manga"])!!)
|
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 {
|
class SearchNode(override val map: Map<String, Any?>): RequestableNode {
|
||||||
|
|
||||||
val manga_css: String by map
|
val manga_css: String by map
|
||||||
|
@ -6,7 +6,6 @@ import retrofit2.converter.gson.GsonConverterFactory
|
|||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to connect with the Github API.
|
* Used to connect with the Github API.
|
||||||
*/
|
*/
|
||||||
|
@ -1,20 +1,25 @@
|
|||||||
package eu.kanade.tachiyomi.data.updater
|
package eu.kanade.tachiyomi.data.updater
|
||||||
|
|
||||||
import android.content.Context
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.util.toast
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
|
class GithubUpdateChecker() {
|
||||||
|
|
||||||
class GithubUpdateChecker(private val context: Context) {
|
private val service: GithubService = GithubService.create()
|
||||||
|
|
||||||
val service: GithubService = GithubService.create()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns observable containing release information
|
* Returns observable containing release information
|
||||||
*/
|
*/
|
||||||
fun checkForApplicationUpdate(): Observable<GithubRelease> {
|
fun checkForUpdate(): Observable<GithubUpdateResult> {
|
||||||
context.toast(R.string.update_check_look_for_updates)
|
return service.getLatestVersion().map { release ->
|
||||||
return service.getLatestVersion()
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.updater
|
||||||
|
|
||||||
|
sealed class GithubUpdateResult {
|
||||||
|
|
||||||
|
class NewUpdate(val release: GithubRelease): GithubUpdateResult()
|
||||||
|
class NoNewUpdate(): GithubUpdateResult()
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -41,6 +41,8 @@ class BackupFragment : BaseRxFragment<BackupPresenter>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedState: Bundle?) {
|
override fun onViewCreated(view: View, savedState: Bundle?) {
|
||||||
|
setToolbarTitle(getString(R.string.label_backup))
|
||||||
|
|
||||||
(activity as ActivityMixin).requestPermissionsOnMarshmallow()
|
(activity as ActivityMixin).requestPermissionsOnMarshmallow()
|
||||||
subscriptions = SubscriptionList()
|
subscriptions = SubscriptionList()
|
||||||
|
|
||||||
@ -121,9 +123,9 @@ class BackupFragment : BaseRxFragment<BackupPresenter>() {
|
|||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe({
|
.subscribe({
|
||||||
presenter.restoreBackup(it)
|
presenter.restoreBackup(it)
|
||||||
}, {
|
}, { error ->
|
||||||
context.toast(it.message)
|
context.toast(error.message)
|
||||||
Timber.e(it, it.message)
|
Timber.e(error)
|
||||||
})
|
})
|
||||||
.apply { subscriptions.add(this) }
|
.apply { subscriptions.add(this) }
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ abstract class FlexibleViewHolder(view: View,
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun toggleActivation() {
|
fun toggleActivation() {
|
||||||
itemView.isActivated = adapter.isSelected(adapterPosition)
|
itemView.isActivated = adapter.isSelected(adapterPosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,13 +2,13 @@ package eu.kanade.tachiyomi.ui.catalogue
|
|||||||
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.support.design.widget.Snackbar
|
||||||
import android.support.v7.widget.GridLayoutManager
|
import android.support.v7.widget.GridLayoutManager
|
||||||
import android.support.v7.widget.LinearLayoutManager
|
import android.support.v7.widget.LinearLayoutManager
|
||||||
import android.support.v7.widget.SearchView
|
import android.support.v7.widget.SearchView
|
||||||
import android.support.v7.widget.Toolbar
|
import android.support.v7.widget.Toolbar
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import android.view.animation.AnimationUtils
|
import android.view.animation.AnimationUtils
|
||||||
import android.widget.AdapterView
|
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import android.widget.Spinner
|
import android.widget.Spinner
|
||||||
@ -16,7 +16,7 @@ import com.afollestad.materialdialogs.MaterialDialog
|
|||||||
import com.f2prateek.rx.preferences.Preference
|
import com.f2prateek.rx.preferences.Preference
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
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.adapter.FlexibleViewHolder
|
||||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
|
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
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.util.toast
|
||||||
import eu.kanade.tachiyomi.widget.DividerItemDecoration
|
import eu.kanade.tachiyomi.widget.DividerItemDecoration
|
||||||
import eu.kanade.tachiyomi.widget.EndlessScrollListener
|
import eu.kanade.tachiyomi.widget.EndlessScrollListener
|
||||||
|
import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener
|
||||||
import kotlinx.android.synthetic.main.fragment_catalogue.*
|
import kotlinx.android.synthetic.main.fragment_catalogue.*
|
||||||
import kotlinx.android.synthetic.main.toolbar.*
|
import kotlinx.android.synthetic.main.toolbar.*
|
||||||
import nucleus.factory.RequiresPresenter
|
import nucleus.factory.RequiresPresenter
|
||||||
@ -40,12 +41,12 @@ import java.util.concurrent.TimeUnit.MILLISECONDS
|
|||||||
* Uses R.layout.fragment_catalogue.
|
* Uses R.layout.fragment_catalogue.
|
||||||
*/
|
*/
|
||||||
@RequiresPresenter(CataloguePresenter::class)
|
@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.
|
* 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.
|
* Adapter containing the list of manga from the catalogue.
|
||||||
@ -65,7 +66,7 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
|
|||||||
/**
|
/**
|
||||||
* Query of the search box.
|
* Query of the search box.
|
||||||
*/
|
*/
|
||||||
val query: String?
|
private val query: String
|
||||||
get() = presenter.query
|
get() = presenter.query
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -93,11 +94,6 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
|
|||||||
*/
|
*/
|
||||||
private var numColumnsSubscription: Subscription? = null
|
private var numColumnsSubscription: Subscription? = null
|
||||||
|
|
||||||
/**
|
|
||||||
* Display mode of the catalogue (list or grid mode).
|
|
||||||
*/
|
|
||||||
private var displayMode: MenuItem? = null
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search item.
|
* Search item.
|
||||||
*/
|
*/
|
||||||
@ -130,6 +126,14 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedState: Bundle?) {
|
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
|
// Initialize adapter, scroll listener and recycler views
|
||||||
adapter = CatalogueAdapter(this)
|
adapter = CatalogueAdapter(this)
|
||||||
|
|
||||||
@ -145,7 +149,8 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
|
|||||||
catalogue_list.adapter = adapter
|
catalogue_list.adapter = adapter
|
||||||
catalogue_list.layoutManager = llm
|
catalogue_list.layoutManager = llm
|
||||||
catalogue_list.addOnScrollListener(listScrollListener)
|
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) {
|
if (presenter.isListMode) {
|
||||||
switcher.showNext()
|
switcher.showNext()
|
||||||
@ -167,11 +172,10 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
|
|||||||
android.R.layout.simple_spinner_item, presenter.sources)
|
android.R.layout.simple_spinner_item, presenter.sources)
|
||||||
spinnerAdapter.setDropDownViewResource(R.layout.spinner_item)
|
spinnerAdapter.setDropDownViewResource(R.layout.spinner_item)
|
||||||
|
|
||||||
val onItemSelected = object : AdapterView.OnItemSelectedListener {
|
val onItemSelected = IgnoreFirstSpinnerListener { position ->
|
||||||
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
|
|
||||||
val source = spinnerAdapter.getItem(position)
|
val source = spinnerAdapter.getItem(position)
|
||||||
if (!presenter.isValidSource(source)) {
|
if (!presenter.isValidSource(source)) {
|
||||||
spinner.setSelection(selectedIndex)
|
spinner?.setSelection(selectedIndex)
|
||||||
context.toast(R.string.source_requires_login)
|
context.toast(R.string.source_requires_login)
|
||||||
} else if (source != presenter.source) {
|
} else if (source != presenter.source) {
|
||||||
selectedIndex = position
|
selectedIndex = position
|
||||||
@ -179,16 +183,14 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
|
|||||||
glm.scrollToPositionWithOffset(0, 0)
|
glm.scrollToPositionWithOffset(0, 0)
|
||||||
llm.scrollToPositionWithOffset(0, 0)
|
llm.scrollToPositionWithOffset(0, 0)
|
||||||
presenter.setActiveSource(source)
|
presenter.setActiveSource(source)
|
||||||
|
activity.invalidateOptionsMenu()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNothingSelected(parent: AdapterView<*>) {
|
selectedIndex = presenter.sources.indexOf(presenter.source)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
spinner = Spinner(themedContext).apply {
|
spinner = Spinner(themedContext).apply {
|
||||||
adapter = spinnerAdapter
|
adapter = spinnerAdapter
|
||||||
selectedIndex = presenter.sources.indexOf(presenter.source)
|
|
||||||
setSelection(selectedIndex)
|
setSelection(selectedIndex)
|
||||||
onItemSelectedListener = onItemSelected
|
onItemSelectedListener = onItemSelected
|
||||||
}
|
}
|
||||||
@ -206,39 +208,49 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
|
|||||||
searchItem = menu.findItem(R.id.action_search).apply {
|
searchItem = menu.findItem(R.id.action_search).apply {
|
||||||
val searchView = actionView as SearchView
|
val searchView = actionView as SearchView
|
||||||
|
|
||||||
if (!query.isNullOrEmpty()) {
|
if (!query.isBlank()) {
|
||||||
expandActionView()
|
expandActionView()
|
||||||
searchView.setQuery(query, true)
|
searchView.setQuery(query, true)
|
||||||
searchView.clearFocus()
|
searchView.clearFocus()
|
||||||
}
|
}
|
||||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||||
override fun onQueryTextSubmit(query: String): Boolean {
|
override fun onQueryTextSubmit(query: String): Boolean {
|
||||||
onSearchEvent(query, true, false)
|
onSearchEvent(query, true)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onQueryTextChange(newText: String): Boolean {
|
override fun onQueryTextChange(newText: String): Boolean {
|
||||||
onSearchEvent(newText, false, false)
|
onSearchEvent(newText, false)
|
||||||
return true
|
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
|
// 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)
|
val icon = if (presenter.isListMode)
|
||||||
R.drawable.ic_view_module_white_24dp
|
R.drawable.ic_view_module_white_24dp
|
||||||
else
|
else
|
||||||
R.drawable.ic_view_list_white_24dp
|
R.drawable.ic_view_list_white_24dp
|
||||||
setIcon(icon)
|
setIcon(icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.action_display_mode -> swapDisplayMode()
|
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)
|
else -> return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@ -248,7 +260,7 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
queryDebouncerSubscription = queryDebouncerSubject.debounce(SEARCH_TIMEOUT, MILLISECONDS)
|
queryDebouncerSubscription = queryDebouncerSubject.debounce(SEARCH_TIMEOUT, MILLISECONDS)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe { searchWithQuery(it, false) }
|
.subscribe { searchWithQuery(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
@ -261,7 +273,7 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
|
|||||||
searchItem?.let {
|
searchItem?.let {
|
||||||
if (it.isActionViewExpanded) it.collapseActionView()
|
if (it.isActionViewExpanded) it.collapseActionView()
|
||||||
}
|
}
|
||||||
toolbar.removeView(spinner)
|
spinner?.let { toolbar.removeView(it) }
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,9 +283,9 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
|
|||||||
* @param query the new query.
|
* @param query the new query.
|
||||||
* @param now whether to send the network call now or debounce it by [SEARCH_TIMEOUT].
|
* @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) {
|
if (now) {
|
||||||
searchWithQuery(query, forceRequest)
|
searchWithQuery(query)
|
||||||
} else {
|
} else {
|
||||||
queryDebouncerSubject.onNext(query)
|
queryDebouncerSubject.onNext(query)
|
||||||
}
|
}
|
||||||
@ -284,9 +296,9 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
|
|||||||
*
|
*
|
||||||
* @param newQuery the new query.
|
* @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 text didn't change, do nothing
|
||||||
if (query == newQuery && !forceRequest)
|
if (query == newQuery)
|
||||||
return
|
return
|
||||||
|
|
||||||
showProgressBar()
|
showProgressBar()
|
||||||
@ -314,7 +326,7 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
|
|||||||
*/
|
*/
|
||||||
fun onAddPage(page: Int, mangas: List<Manga>) {
|
fun onAddPage(page: Int, mangas: List<Manga>) {
|
||||||
hideProgressBar()
|
hideProgressBar()
|
||||||
if (page == 0) {
|
if (page == 1) {
|
||||||
adapter.clear()
|
adapter.clear()
|
||||||
gridScrollListener.resetScroll()
|
gridScrollListener.resetScroll()
|
||||||
listScrollListener.resetScroll()
|
listScrollListener.resetScroll()
|
||||||
@ -329,12 +341,12 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
|
|||||||
*/
|
*/
|
||||||
fun onAddPageError(error: Throwable) {
|
fun onAddPageError(error: Throwable) {
|
||||||
hideProgressBar()
|
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) {
|
setAction(R.string.action_retry) {
|
||||||
showProgressBar()
|
showProgressBar()
|
||||||
presenter.retryPage()
|
presenter.requestNext()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -354,11 +366,7 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
|
|||||||
fun swapDisplayMode() {
|
fun swapDisplayMode() {
|
||||||
presenter.swapDisplayMode()
|
presenter.swapDisplayMode()
|
||||||
val isListMode = presenter.isListMode
|
val isListMode = presenter.isListMode
|
||||||
val icon = if (isListMode)
|
activity.invalidateOptionsMenu()
|
||||||
R.drawable.ic_view_module_white_24dp
|
|
||||||
else
|
|
||||||
R.drawable.ic_view_list_white_24dp
|
|
||||||
displayMode?.setIcon(icon)
|
|
||||||
switcher.showNext()
|
switcher.showNext()
|
||||||
if (!isListMode) {
|
if (!isListMode) {
|
||||||
// Initialize mangas if going to grid view
|
// Initialize mangas if going to grid view
|
||||||
@ -446,4 +454,27 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
|
|||||||
}.show()
|
}.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()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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 }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.data.source.online.LoginSource
|
import eu.kanade.tachiyomi.data.source.online.LoginSource
|
||||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
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.ui.base.presenter.BasePresenter
|
||||||
import eu.kanade.tachiyomi.util.RxPager
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
import rx.Subscription
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import rx.subjects.PublishSubject
|
import rx.subjects.PublishSubject
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.util.NoSuchElementException
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Presenter of [CatalogueFragment].
|
* Presenter of [CatalogueFragment].
|
||||||
*/
|
*/
|
||||||
class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Source manager.
|
* Source manager.
|
||||||
@ -64,14 +66,14 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
|||||||
private set
|
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.
|
* Subject that initializes a list of manga.
|
||||||
@ -84,80 +86,93 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
|||||||
var isListMode: Boolean = false
|
var isListMode: Boolean = false
|
||||||
private set
|
private set
|
||||||
|
|
||||||
companion object {
|
|
||||||
/**
|
/**
|
||||||
* Id of the restartable that delivers a list of manga.
|
* Subscription for the pager.
|
||||||
*/
|
*/
|
||||||
const val PAGER = 1
|
private var pagerSubscription: Subscription? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Id of the restartable that requests a page of manga from network.
|
* Subscription for one request from the pager.
|
||||||
*/
|
*/
|
||||||
const val REQUEST_PAGE = 2
|
private var pageSubscription: Subscription? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Id of the restartable that initializes the details of manga.
|
* Subscription to initialize manga details.
|
||||||
*/
|
*/
|
||||||
const val GET_MANGA_DETAILS = 3
|
private var initializerSubscription: Subscription? = null
|
||||||
|
|
||||||
/**
|
|
||||||
* Key to save and restore [query] from a [Bundle].
|
|
||||||
*/
|
|
||||||
const val QUERY_KEY = "query_key"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
|
|
||||||
|
try {
|
||||||
source = getLastUsedSource()
|
source = getLastUsedSource()
|
||||||
|
} catch (error: NoSuchElementException) {
|
||||||
if (savedState != null) {
|
return
|
||||||
query = savedState.getString(QUERY_KEY, "")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
startableLatestCache(GET_MANGA_DETAILS,
|
if (savedState != null) {
|
||||||
{ mangaDetailSubject.observeOn(Schedulers.io())
|
query = savedState.getString(CataloguePresenter::query.name, "")
|
||||||
.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) })
|
|
||||||
|
|
||||||
add(prefs.catalogueAsList().asObservable()
|
add(prefs.catalogueAsList().asObservable()
|
||||||
.subscribe { setDisplayMode(it) })
|
.subscribe { setDisplayMode(it) })
|
||||||
|
|
||||||
startableReplay(PAGER,
|
restartPager()
|
||||||
{ 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSave(state: Bundle) {
|
override fun onSave(state: Bundle) {
|
||||||
state.putString(QUERY_KEY, query)
|
state.putString(CataloguePresenter::query.name, query)
|
||||||
super.onSave(state)
|
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) {
|
fun restartPager(query: String = this.query, filters: List<Filter> = this.filters) {
|
||||||
isListMode = asList
|
this.query = query
|
||||||
if (asList) {
|
this.filters = filters
|
||||||
stop(GET_MANGA_DETAILS)
|
|
||||||
} else {
|
if (!isListMode) {
|
||||||
start(GET_MANGA_DETAILS)
|
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) {
|
fun setActiveSource(source: OnlineSource) {
|
||||||
prefs.lastUsedCatalogueSource().set(source.id)
|
prefs.lastUsedCatalogueSource().set(source.id)
|
||||||
this.source = source
|
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 = "") {
|
private fun setDisplayMode(asList: Boolean) {
|
||||||
this.query = query
|
isListMode = asList
|
||||||
stop(REQUEST_PAGE)
|
if (asList) {
|
||||||
lastMangasPage = null
|
initializerSubscription?.let { remove(it) }
|
||||||
|
} else {
|
||||||
if (!isListMode) {
|
subscribeToMangaInitializer()
|
||||||
start(GET_MANGA_DETAILS)
|
|
||||||
}
|
|
||||||
start(PAGER)
|
|
||||||
start(REQUEST_PAGE)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Requests the next page for the active pager.
|
|
||||||
*/
|
|
||||||
fun requestNext() {
|
|
||||||
if (hasNextPage()) {
|
|
||||||
start(REQUEST_PAGE)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 {
|
private fun subscribeToMangaInitializer() {
|
||||||
return lastMangasPage?.nextPageUrl != null
|
initializerSubscription?.let { remove(it) }
|
||||||
}
|
initializerSubscription = mangaDetailSubject.observeOn(Schedulers.io())
|
||||||
|
.flatMap { Observable.from(it) }
|
||||||
/**
|
.filter { !it.initialized }
|
||||||
* Retries the current request that failed.
|
.concatMap { getMangaDetailsObservable(it) }
|
||||||
*/
|
.onBackpressureBuffer()
|
||||||
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) }
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.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.
|
* @param source the source to check.
|
||||||
* @return true if the source is valid, false otherwise.
|
* @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 == null) return false
|
||||||
|
|
||||||
if (source is LoginSource) {
|
if (source is LoginSource) {
|
||||||
@ -321,8 +327,9 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
|||||||
/**
|
/**
|
||||||
* Returns a list of enabled sources ordered by language and name.
|
* 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 languages = prefs.enabledLanguages().getOrDefault()
|
||||||
|
val hiddenCatalogues = prefs.hiddenCatalogues().getOrDefault()
|
||||||
|
|
||||||
// Ensure at least one language
|
// Ensure at least one language
|
||||||
if (languages.isEmpty()) {
|
if (languages.isEmpty()) {
|
||||||
@ -331,6 +338,7 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
|||||||
|
|
||||||
return sourceManager.getOnlineSources()
|
return sourceManager.getOnlineSources()
|
||||||
.filter { it.lang.code in languages }
|
.filter { it.lang.code in languages }
|
||||||
|
.filterNot { it.id.toString() in hiddenCatalogues }
|
||||||
.sortedBy { "(${it.lang.code}) ${it.name}" }
|
.sortedBy { "(${it.lang.code}) ${it.name}" }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -354,4 +362,17 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
|||||||
prefs.catalogueAsList().set(!isListMode)
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
25
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/Pager.kt
Normal file
25
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/Pager.kt
Normal 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>
|
||||||
|
}
|
@ -55,9 +55,9 @@ class CategoryActivity : BaseRxActivity<CategoryPresenter>(), ActionMode.Callbac
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
setAppTheme()
|
setAppTheme()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedState)
|
||||||
|
|
||||||
// Inflate activity_edit_categories.xml.
|
// Inflate activity_edit_categories.xml.
|
||||||
setContentView(R.layout.activity_edit_categories)
|
setContentView(R.layout.activity_edit_categories)
|
||||||
|
@ -35,7 +35,7 @@ class DownloadPresenter : BasePresenter<DownloadFragment>() {
|
|||||||
.subscribeLatestCache({ view, downloads ->
|
.subscribeLatestCache({ view, downloads ->
|
||||||
view.onNextDownloads(downloads)
|
view.onNextDownloads(downloads)
|
||||||
}, { view, error ->
|
}, { view, error ->
|
||||||
Timber.e(error, error.message)
|
Timber.e(error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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 }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,23 +1,23 @@
|
|||||||
package eu.kanade.tachiyomi.ui.library
|
package eu.kanade.tachiyomi.ui.library
|
||||||
|
|
||||||
import android.support.v4.app.Fragment
|
import android.view.View
|
||||||
import android.support.v4.app.FragmentManager
|
import android.view.ViewGroup
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
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.
|
* This adapter stores the categories from the library, used with a ViewPager.
|
||||||
*
|
*
|
||||||
* @param fm the fragment manager.
|
|
||||||
* @constructor creates an instance of the adapter.
|
* @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.
|
* 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.
|
// This setter helps to not refresh the adapter if the reference to the list doesn't change.
|
||||||
set(value) {
|
set(value) {
|
||||||
if (field !== 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 new view.
|
||||||
* @return a fragment for the given position.
|
|
||||||
*/
|
*/
|
||||||
override fun getItem(position: Int): Fragment {
|
override fun createView(container: ViewGroup): View {
|
||||||
return LibraryCategoryFragment.newInstance(position)
|
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.
|
* @return the number of categories or 0 if the list is null.
|
||||||
*/
|
*/
|
||||||
override fun getCount(): Int {
|
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.
|
* @return the title to display.
|
||||||
*/
|
*/
|
||||||
override fun getPageTitle(position: Int): CharSequence {
|
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
|
* Returns the position of the view.
|
||||||
* fragments.
|
|
||||||
*
|
|
||||||
* @param mode the mode to set.
|
|
||||||
*/
|
*/
|
||||||
fun setSelectionMode(mode: Int) {
|
override fun getItemPosition(obj: Any?): Int {
|
||||||
for (fragment in getRegisteredFragments()) {
|
val view = obj as? LibraryCategoryView ?: return POSITION_NONE
|
||||||
(fragment as LibraryCategoryFragment).setSelectionMode(mode)
|
val index = categories.indexOfFirst { it.id == view.category.id }
|
||||||
}
|
return if (index == -1) POSITION_NONE else index
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notifies the adapters in all the registered fragments to refresh their content.
|
|
||||||
*/
|
|
||||||
fun refreshRegisteredAdapters() {
|
|
||||||
for (fragment in getRegisteredFragments()) {
|
|
||||||
(fragment as LibraryCategoryFragment).adapter.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -8,7 +8,7 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
|
|||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.util.inflate
|
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 kotlinx.android.synthetic.main.item_catalogue_grid.view.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ import java.util.*
|
|||||||
*
|
*
|
||||||
* @param fragment the fragment containing this adapter.
|
* @param fragment the fragment containing this adapter.
|
||||||
*/
|
*/
|
||||||
class LibraryCategoryAdapter(val fragment: LibraryCategoryFragment) :
|
class LibraryCategoryAdapter(val fragment: LibraryCategoryView) :
|
||||||
FlexibleAdapter<LibraryHolder, Manga>() {
|
FlexibleAdapter<LibraryHolder, Manga>() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -84,11 +84,18 @@ class LibraryCategoryAdapter(val fragment: LibraryCategoryFragment) :
|
|||||||
* @return a new view holder for a manga.
|
* @return a new view holder for a manga.
|
||||||
*/
|
*/
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LibraryHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LibraryHolder {
|
||||||
|
// 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 view = parent.inflate(R.layout.item_catalogue_grid).apply {
|
||||||
|
val coverHeight = parent.itemWidth / 3 * 4
|
||||||
card.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight)
|
card.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight)
|
||||||
gradient.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight / 2, Gravity.BOTTOM)
|
gradient.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight / 2, Gravity.BOTTOM)
|
||||||
}
|
}
|
||||||
return LibraryHolder(view, this, fragment)
|
return LibraryGridHolder(view, this, fragment)
|
||||||
|
} else {
|
||||||
|
val view = parent.inflate(R.layout.item_library_list)
|
||||||
|
return LibraryListHolder(view, this, fragment)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -101,14 +108,17 @@ class LibraryCategoryAdapter(val fragment: LibraryCategoryFragment) :
|
|||||||
val manga = getItem(position)
|
val manga = getItem(position)
|
||||||
|
|
||||||
holder.onSetValues(manga)
|
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)
|
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
|
fun indexOf(manga: Manga): Int {
|
||||||
get() = fragment.recycler.itemWidth / 3 * 4
|
return mangas.orEmpty().indexOfFirst { it.id == manga.id }
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
|
|
||||||
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.library
|
|||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.design.widget.TabLayout
|
import android.support.design.widget.TabLayout
|
||||||
import android.support.v4.view.ViewPager
|
import android.support.v4.view.ViewPager
|
||||||
@ -9,12 +10,13 @@ import android.support.v7.view.ActionMode
|
|||||||
import android.support.v7.widget.SearchView
|
import android.support.v7.widget.SearchView
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
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.R
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
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.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.data.source.online.english.EHentai
|
import eu.kanade.tachiyomi.data.source.online.english.EHentai
|
||||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
|
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.activity_main.*
|
||||||
import kotlinx.android.synthetic.main.fragment_library.*
|
import kotlinx.android.synthetic.main.fragment_library.*
|
||||||
import nucleus.factory.RequiresPresenter
|
import nucleus.factory.RequiresPresenter
|
||||||
|
import rx.Subscription
|
||||||
|
import timber.log.Timber
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -40,6 +45,11 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
|||||||
lateinit var adapter: LibraryAdapter
|
lateinit var adapter: LibraryAdapter
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preferences.
|
||||||
|
*/
|
||||||
|
val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TabLayout of the categories.
|
* TabLayout of the categories.
|
||||||
*/
|
*/
|
||||||
@ -59,8 +69,7 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
|||||||
/**
|
/**
|
||||||
* Action mode for manga selection.
|
* Action mode for manga selection.
|
||||||
*/
|
*/
|
||||||
var actionMode: ActionMode? = null
|
private var actionMode: ActionMode? = null
|
||||||
private set
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Selected manga for editing its cover.
|
* Selected manga for editing its cover.
|
||||||
@ -79,6 +88,17 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
|||||||
|
|
||||||
lateinit var favoritesSyncManager: FavoritesSyncManager
|
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 {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* Key to change the cover of a manga in [onActivityResult].
|
* 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?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
setHasOptionsMenu(true)
|
setHasOptionsMenu(true)
|
||||||
isFilterDownloaded = presenter.preferences.filterDownloaded().get() as Boolean
|
isFilterDownloaded = preferences.filterDownloaded().get() as Boolean
|
||||||
isFilterUnread = presenter.preferences.filterUnread().get() as Boolean
|
isFilterUnread = preferences.filterUnread().get() as Boolean
|
||||||
favoritesSyncManager = FavoritesSyncManager(context, DatabaseHelper(context))
|
favoritesSyncManager = FavoritesSyncManager(context, DatabaseHelper(context))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,11 +140,11 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
|||||||
override fun onViewCreated(view: View, savedState: Bundle?) {
|
override fun onViewCreated(view: View, savedState: Bundle?) {
|
||||||
setToolbarTitle(getString(R.string.label_library))
|
setToolbarTitle(getString(R.string.label_library))
|
||||||
|
|
||||||
adapter = LibraryAdapter(childFragmentManager)
|
adapter = LibraryAdapter(this)
|
||||||
view_pager.adapter = adapter
|
view_pager.adapter = adapter
|
||||||
view_pager.addOnPageChangeListener(object : ViewPager.SimpleOnPageChangeListener() {
|
view_pager.addOnPageChangeListener(object : ViewPager.SimpleOnPageChangeListener() {
|
||||||
override fun onPageSelected(position: Int) {
|
override fun onPageSelected(position: Int) {
|
||||||
presenter.preferences.lastUsedCategory().set(position)
|
preferences.lastUsedCategory().set(position)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
tabs.setupWithViewPager(view_pager)
|
tabs.setupWithViewPager(view_pager)
|
||||||
@ -133,9 +153,18 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
|||||||
activeCategory = savedState.getInt(CATEGORY_KEY)
|
activeCategory = savedState.getInt(CATEGORY_KEY)
|
||||||
query = savedState.getString(QUERY_KEY)
|
query = savedState.getString(QUERY_KEY)
|
||||||
presenter.searchSubject.onNext(query)
|
presenter.searchSubject.onNext(query)
|
||||||
} else {
|
if (presenter.selectedMangas.isNotEmpty()) {
|
||||||
activeCategory = presenter.preferences.lastUsedCategory().getOrDefault()
|
createActionModeIfNeeded()
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
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() {
|
override fun onResume() {
|
||||||
@ -144,6 +173,7 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
|
numColumnsSubscription?.unsubscribe()
|
||||||
tabs.setupWithViewPager(null)
|
tabs.setupWithViewPager(null)
|
||||||
tabs.visibility = View.GONE
|
tabs.visibility = View.GONE
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
@ -184,6 +214,7 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
@ -192,7 +223,7 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
|||||||
// Change unread filter status.
|
// Change unread filter status.
|
||||||
isFilterUnread = !isFilterUnread
|
isFilterUnread = !isFilterUnread
|
||||||
// Update settings.
|
// Update settings.
|
||||||
presenter.preferences.filterUnread().set(isFilterUnread)
|
preferences.filterUnread().set(isFilterUnread)
|
||||||
// Apply filter.
|
// Apply filter.
|
||||||
onFilterCheckboxChanged()
|
onFilterCheckboxChanged()
|
||||||
}
|
}
|
||||||
@ -200,7 +231,7 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
|||||||
// Change downloaded filter status.
|
// Change downloaded filter status.
|
||||||
isFilterDownloaded = !isFilterDownloaded
|
isFilterDownloaded = !isFilterDownloaded
|
||||||
// Update settings.
|
// Update settings.
|
||||||
presenter.preferences.filterDownloaded().set(isFilterDownloaded)
|
preferences.filterDownloaded().set(isFilterDownloaded)
|
||||||
// Apply filter.
|
// Apply filter.
|
||||||
onFilterCheckboxChanged()
|
onFilterCheckboxChanged()
|
||||||
}
|
}
|
||||||
@ -209,14 +240,14 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
|||||||
isFilterUnread = false
|
isFilterUnread = false
|
||||||
isFilterDownloaded = false
|
isFilterDownloaded = false
|
||||||
// Update settings.
|
// Update settings.
|
||||||
presenter.preferences.filterUnread().set(isFilterUnread)
|
preferences.filterUnread().set(isFilterUnread)
|
||||||
presenter.preferences.filterDownloaded().set(isFilterDownloaded)
|
preferences.filterDownloaded().set(isFilterDownloaded)
|
||||||
// Apply filter
|
// Apply filter
|
||||||
onFilterCheckboxChanged()
|
onFilterCheckboxChanged()
|
||||||
}
|
}
|
||||||
// R.id.action_update_library -> {
|
R.id.action_library_display_mode -> swapDisplayMode()
|
||||||
// LibraryUpdateService.start(activity, true)
|
//R.id.action_update_library -> {
|
||||||
// }
|
// LibraryUpdateService.start(activity)
|
||||||
R.id.action_sync -> {
|
R.id.action_sync -> {
|
||||||
favoritesSyncManager.guiSyncFavorites({
|
favoritesSyncManager.guiSyncFavorites({
|
||||||
(activity as MainActivity).setFragment(LibraryFragment.newInstance(), 0)
|
(activity as MainActivity).setFragment(LibraryFragment.newInstance(), 0)
|
||||||
@ -236,12 +267,41 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
|||||||
* Applies filter change
|
* Applies filter change
|
||||||
*/
|
*/
|
||||||
private fun onFilterCheckboxChanged() {
|
private fun onFilterCheckboxChanged() {
|
||||||
presenter.updateLibrary()
|
presenter.resubscribeLibrary()
|
||||||
adapter.notifyDataSetChanged()
|
|
||||||
adapter.refreshRegisteredAdapters()
|
|
||||||
activity.supportInvalidateOptionsMenu()
|
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.
|
* Updates the query.
|
||||||
*
|
*
|
||||||
@ -268,7 +328,7 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
|||||||
R.string.information_empty_library, R.drawable.ic_book_black_128dp)
|
R.string.information_empty_library, R.drawable.ic_book_black_128dp)
|
||||||
|
|
||||||
// Get the current active category.
|
// 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
|
// Set the categories
|
||||||
adapter.categories = categories
|
adapter.categories = categories
|
||||||
@ -284,31 +344,42 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the title of the action mode.
|
* Creates the action mode if it's not created already.
|
||||||
*
|
|
||||||
* @param count the number of items selected.
|
|
||||||
*/
|
*/
|
||||||
fun setContextTitle(count: Int) {
|
fun createActionModeIfNeeded() {
|
||||||
actionMode?.title = getString(R.string.label_selected, count)
|
if (actionMode == null) {
|
||||||
|
actionMode = activity.startSupportActionMode(this)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the visibility of the edit cover item.
|
* Destroys the action mode.
|
||||||
*
|
|
||||||
* @param count the number of items selected.
|
|
||||||
*/
|
*/
|
||||||
fun setVisibilityOfCoverEdit(count: Int) {
|
fun destroyActionModeIfNeeded() {
|
||||||
// If count = 1 display edit button
|
actionMode?.finish()
|
||||||
actionMode?.menu?.findItem(R.id.action_edit_cover)?.isVisible = count == 1
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidates the action mode, forcing it to refresh its content.
|
||||||
|
*/
|
||||||
|
fun invalidateActionMode() {
|
||||||
|
actionMode?.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
mode.menuInflater.inflate(R.menu.library_selection, menu)
|
mode.menuInflater.inflate(R.menu.library_selection, menu)
|
||||||
adapter.setSelectionMode(FlexibleAdapter.MODE_MULTI)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -327,18 +398,10 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyActionMode(mode: ActionMode) {
|
override fun onDestroyActionMode(mode: ActionMode) {
|
||||||
adapter.setSelectionMode(FlexibleAdapter.MODE_SINGLE)
|
presenter.clearSelections()
|
||||||
presenter.selectedMangas.clear()
|
|
||||||
actionMode = null
|
actionMode = null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Destroys the action mode.
|
|
||||||
*/
|
|
||||||
fun destroyActionModeIfNeeded() {
|
|
||||||
actionMode?.finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Changes the cover for the selected manga.
|
* Changes the cover for the selected manga.
|
||||||
*
|
*
|
||||||
@ -368,14 +431,14 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
|||||||
context.contentResolver.openInputStream(data.data).use {
|
context.contentResolver.openInputStream(data.data).use {
|
||||||
// Update cover to selected file, show error if something went wrong
|
// Update cover to selected file, show error if something went wrong
|
||||||
if (presenter.editCoverWithStream(it, manga)) {
|
if (presenter.editCoverWithStream(it, manga)) {
|
||||||
adapter.refreshRegisteredAdapters()
|
// TODO refresh cover
|
||||||
} else {
|
} else {
|
||||||
context.toast(R.string.notification_manga_update_failed)
|
context.toast(R.string.notification_manga_update_failed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (error: IOException) {
|
||||||
context.toast(R.string.notification_manga_update_failed)
|
context.toast(R.string.notification_manga_update_failed)
|
||||||
e.printStackTrace()
|
Timber.e(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -422,20 +485,4 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
|||||||
.show()
|
.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()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,23 +1,18 @@
|
|||||||
package eu.kanade.tachiyomi.ui.library
|
package eu.kanade.tachiyomi.ui.library
|
||||||
|
|
||||||
import android.view.View
|
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.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
|
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.
|
* Generic class used to hold the displayed data of a manga in the library.
|
||||||
* All the elements from the layout file "item_catalogue_grid" are available in this class.
|
|
||||||
*
|
|
||||||
* @param view the inflated view for this holder.
|
* @param view the inflated view for this holder.
|
||||||
* @param adapter the adapter handling this holder.
|
* @param adapter the adapter handling this holder.
|
||||||
* @param listener a listener to react to single tap and long tap events.
|
* @param listener a listener to react to the single tap and long tap events.
|
||||||
* @constructor creates a new library holder.
|
|
||||||
*/
|
*/
|
||||||
class LibraryHolder(private val view: View,
|
|
||||||
private val adapter: LibraryCategoryAdapter,
|
abstract class LibraryHolder(private val view: View,
|
||||||
|
adapter: LibraryCategoryAdapter,
|
||||||
listener: FlexibleViewHolder.OnListItemClickListener)
|
listener: FlexibleViewHolder.OnListItemClickListener)
|
||||||
: FlexibleViewHolder(view, adapter, listener) {
|
: FlexibleViewHolder(view, adapter, listener) {
|
||||||
|
|
||||||
@ -27,23 +22,6 @@ class LibraryHolder(private val view: View,
|
|||||||
*
|
*
|
||||||
* @param manga the manga to bind.
|
* @param manga the manga to bind.
|
||||||
*/
|
*/
|
||||||
fun onSetValues(manga: Manga) {
|
abstract 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -16,6 +16,7 @@ import rx.Observable
|
|||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import rx.subjects.BehaviorSubject
|
import rx.subjects.BehaviorSubject
|
||||||
|
import rx.subjects.PublishSubject
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
@ -29,22 +30,27 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
|
|||||||
/**
|
/**
|
||||||
* Categories of the library.
|
* Categories of the library.
|
||||||
*/
|
*/
|
||||||
lateinit var categories: List<Category>
|
var categories: List<Category> = emptyList()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Currently selected manga.
|
* Currently selected manga.
|
||||||
*/
|
*/
|
||||||
var selectedMangas = mutableListOf<Manga>()
|
val selectedMangas = mutableListOf<Manga>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search query of the library.
|
* 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.
|
* 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.
|
* Database.
|
||||||
@ -149,7 +155,7 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
|
|||||||
/**
|
/**
|
||||||
* Resubscribes to library.
|
* Resubscribes to library.
|
||||||
*/
|
*/
|
||||||
fun updateLibrary() {
|
fun resubscribeLibrary() {
|
||||||
start(GET_LIBRARY)
|
start(GET_LIBRARY)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,17 +225,27 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
|
|||||||
fun setSelection(manga: Manga, selected: Boolean) {
|
fun setSelection(manga: Manga, selected: Boolean) {
|
||||||
if (selected) {
|
if (selected) {
|
||||||
selectedMangas.add(manga)
|
selectedMangas.add(manga)
|
||||||
|
selectionSubject.onNext(LibrarySelectionEvent.Selected(manga))
|
||||||
} else {
|
} else {
|
||||||
selectedMangas.remove(manga)
|
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.
|
* Returns the common categories for the given list of manga.
|
||||||
*
|
*
|
||||||
* @param mangas the 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() }
|
.map { db.getCategoriesForManga(it).executeAsBlocking() }
|
||||||
.reduce { set1: Iterable<Category>, set2 -> set1.intersect(set2) }
|
.reduce { set1: Iterable<Category>, set2 -> set1.intersect(set2) }
|
||||||
|
|
||||||
@ -285,4 +301,12 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the active display mode.
|
||||||
|
*/
|
||||||
|
fun swapDisplayMode() {
|
||||||
|
val displayAsList = preferences.libraryAsList().getOrDefault()
|
||||||
|
preferences.libraryAsList().set(!displayAsList)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
}
|
@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|||||||
import eu.kanade.tachiyomi.ui.backup.BackupFragment
|
import eu.kanade.tachiyomi.ui.backup.BackupFragment
|
||||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||||
import eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment
|
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.download.DownloadFragment
|
||||||
import eu.kanade.tachiyomi.ui.library.LibraryFragment
|
import eu.kanade.tachiyomi.ui.library.LibraryFragment
|
||||||
import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersFragment
|
import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersFragment
|
||||||
@ -60,9 +61,10 @@ class MainActivity : BaseActivity() {
|
|||||||
val id = item.itemId
|
val id = item.itemId
|
||||||
when (id) {
|
when (id) {
|
||||||
R.id.nav_drawer_library -> setFragment(LibraryFragment.newInstance(), 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_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_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_downloads -> setFragment(DownloadFragment.newInstance(), id)
|
||||||
R.id.nav_drawer_settings -> {
|
R.id.nav_drawer_settings -> {
|
||||||
val intent = Intent(this, SettingsActivity::class.java)
|
val intent = Intent(this, SettingsActivity::class.java)
|
||||||
|
@ -23,6 +23,7 @@ class MangaActivity : BaseRxActivity<MangaPresenter>() {
|
|||||||
|
|
||||||
const val FROM_CATALOGUE_EXTRA = "from_catalogue"
|
const val FROM_CATALOGUE_EXTRA = "from_catalogue"
|
||||||
const val MANGA_EXTRA = "manga"
|
const val MANGA_EXTRA = "manga"
|
||||||
|
const val FROM_LAUNCHER_EXTRA = "from_launcher"
|
||||||
const val INFO_FRAGMENT = 0
|
const val INFO_FRAGMENT = 0
|
||||||
const val CHAPTERS_FRAGMENT = 1
|
const val CHAPTERS_FRAGMENT = 1
|
||||||
|
|
||||||
@ -45,6 +46,11 @@ class MangaActivity : BaseRxActivity<MangaPresenter>() {
|
|||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
setContentView(R.layout.activity_manga)
|
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) {
|
presenter.setMangaEvent(SharedData.getOrPut(MangaEvent::class.java) {
|
||||||
val id = intent.getLongExtra(MANGA_EXTRA, 0)
|
val id = intent.getLongExtra(MANGA_EXTRA, 0)
|
||||||
MangaEvent(presenter.db.getManga(id).executeAsBlocking()!!)
|
MangaEvent(presenter.db.getManga(id).executeAsBlocking()!!)
|
||||||
|
@ -116,8 +116,25 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
|
|||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
inflater.inflate(R.menu.chapters, menu)
|
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 {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
@ -126,8 +143,14 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
|
|||||||
R.id.manga_download -> showDownloadDialog()
|
R.id.manga_download -> showDownloadDialog()
|
||||||
R.id.action_sorting_mode -> showSortingDialog()
|
R.id.action_sorting_mode -> showSortingDialog()
|
||||||
R.id.action_filter_unread -> {
|
R.id.action_filter_unread -> {
|
||||||
|
item.isChecked = !item.isChecked
|
||||||
|
presenter.setUnreadFilter(item.isChecked)
|
||||||
|
activity.supportInvalidateOptionsMenu()
|
||||||
|
}
|
||||||
|
R.id.action_filter_read -> {
|
||||||
item.isChecked = !item.isChecked
|
item.isChecked = !item.isChecked
|
||||||
presenter.setReadFilter(item.isChecked)
|
presenter.setReadFilter(item.isChecked)
|
||||||
|
activity.supportInvalidateOptionsMenu()
|
||||||
}
|
}
|
||||||
R.id.action_filter_downloaded -> {
|
R.id.action_filter_downloaded -> {
|
||||||
item.isChecked = !item.isChecked
|
item.isChecked = !item.isChecked
|
||||||
@ -145,8 +168,7 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
|
|||||||
|
|
||||||
fun onNextManga(manga: Manga) {
|
fun onNextManga(manga: Manga) {
|
||||||
// Set initial values
|
// Set initial values
|
||||||
setReadFilter()
|
activity.supportInvalidateOptionsMenu()
|
||||||
setDownloadedFilter()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onNextChapters(chapters: List<ChapterModel>) {
|
fun onNextChapters(chapters: List<ChapterModel>) {
|
||||||
@ -242,6 +264,7 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
|
|||||||
|
|
||||||
fun getUnreadChaptersSorted() = presenter.chapters
|
fun getUnreadChaptersSorted() = presenter.chapters
|
||||||
.filter { !it.read && !it.isDownloaded }
|
.filter { !it.read && !it.isDownloaded }
|
||||||
|
.distinctBy { it.name }
|
||||||
.sortedByDescending { it.source_order }
|
.sortedByDescending { it.source_order }
|
||||||
|
|
||||||
// i = 0: Download 1
|
// i = 0: Download 1
|
||||||
@ -354,7 +377,7 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
|
|||||||
|
|
||||||
fun onChaptersDeletedError(error: Throwable) {
|
fun onChaptersDeletedError(error: Throwable) {
|
||||||
dismissDeletingDialog()
|
dismissDeletingDialog()
|
||||||
Timber.e(error, error.message)
|
Timber.e(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dismissDeletingDialog() {
|
fun dismissDeletingDialog() {
|
||||||
@ -394,12 +417,4 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
|
|||||||
private fun setContextTitle(count: Int) {
|
private fun setContextTitle(count: Int) {
|
||||||
actionMode?.title = getString(R.string.label_selected, count)
|
actionMode?.title = getString(R.string.label_selected, count)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setReadFilter() {
|
|
||||||
activity.supportInvalidateOptionsMenu()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setDownloadedFilter() {
|
|
||||||
activity.supportInvalidateOptionsMenu()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -111,7 +111,7 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
|
|||||||
startableLatestCache(CHAPTER_STATUS_CHANGES,
|
startableLatestCache(CHAPTER_STATUS_CHANGES,
|
||||||
{ getChapterStatusObservable() },
|
{ getChapterStatusObservable() },
|
||||||
{ view, download -> view.onChapterStatusChange(download) },
|
{ 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.
|
// Find the active manga from the shared data or return.
|
||||||
manga = SharedData.get(MangaEvent::class.java)?.manga ?: return
|
manga = SharedData.get(MangaEvent::class.java)?.manga ?: return
|
||||||
@ -209,6 +209,9 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
|
|||||||
if (onlyUnread()) {
|
if (onlyUnread()) {
|
||||||
observable = observable.filter { !it.read }
|
observable = observable.filter { !it.read }
|
||||||
}
|
}
|
||||||
|
if (onlyRead()) {
|
||||||
|
observable = observable.filter { it.read }
|
||||||
|
}
|
||||||
if (onlyDownloaded()) {
|
if (onlyDownloaded()) {
|
||||||
observable = observable.filter { it.isDownloaded }
|
observable = observable.filter { it.isDownloaded }
|
||||||
}
|
}
|
||||||
@ -349,12 +352,23 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
|
|||||||
*
|
*
|
||||||
* @param onlyUnread whether to display only unread chapters or all chapters.
|
* @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
|
manga.readFilter = if (onlyUnread) Manga.SHOW_UNREAD else Manga.SHOW_ALL
|
||||||
db.updateFlags(manga).executeAsBlocking()
|
db.updateFlags(manga).executeAsBlocking()
|
||||||
refreshChapters()
|
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.
|
* Sets the download filter and requests an UI update.
|
||||||
*
|
*
|
||||||
@ -411,6 +425,13 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
|
|||||||
return manga.readFilter == Manga.SHOW_UNREAD
|
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.
|
* Whether the sorting method is descending or ascending.
|
||||||
*/
|
*/
|
||||||
|
@ -1,20 +1,34 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga.info
|
package eu.kanade.tachiyomi.ui.manga.info
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.Bitmap
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.customtabs.CustomTabsIntent
|
import android.support.customtabs.CustomTabsIntent
|
||||||
import android.view.*
|
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.Glide
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
|
import com.bumptech.glide.load.resource.bitmap.CenterCrop
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.source.Source
|
import eu.kanade.tachiyomi.data.source.Source
|
||||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
||||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
|
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.getResourceColor
|
||||||
import eu.kanade.tachiyomi.util.toast
|
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 kotlinx.android.synthetic.main.fragment_manga_info.*
|
||||||
import nucleus.factory.RequiresPresenter
|
import nucleus.factory.RequiresPresenter
|
||||||
|
import rx.Observable
|
||||||
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
|
import rx.schedulers.Schedulers
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fragment that shows manga information.
|
* Fragment that shows manga information.
|
||||||
@ -33,6 +47,7 @@ class MangaInfoFragment : BaseRxFragment<MangaInfoPresenter>() {
|
|||||||
fun newInstance(): MangaInfoFragment {
|
fun newInstance(): MangaInfoFragment {
|
||||||
return MangaInfoFragment()
|
return MangaInfoFragment()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
@ -59,6 +74,8 @@ class MangaInfoFragment : BaseRxFragment<MangaInfoPresenter>() {
|
|||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.action_open_in_browser -> openInBrowser()
|
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)
|
else -> return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
return true
|
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.
|
* Update FAB with correct drawable.
|
||||||
*
|
*
|
||||||
|
@ -128,5 +128,4 @@ class MangaInfoPresenter : BasePresenter<MangaInfoFragment>() {
|
|||||||
private fun refreshManga() {
|
private fun refreshManga() {
|
||||||
start(GET_MANGA)
|
start(GET_MANGA)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -42,9 +42,9 @@ class ChapterLoader(
|
|||||||
.repeat()
|
.repeat()
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.subscribe({
|
.subscribe({
|
||||||
}, {
|
}, { error ->
|
||||||
if (it !is InterruptedException) {
|
if (error !is InterruptedException) {
|
||||||
Timber.e(it, it.message)
|
Timber.e(error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -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.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
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.base.activity.BaseRxActivity
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader
|
import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.LeftToRightReader
|
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 me.zhanghai.android.systemuihelper.SystemUiHelper.*
|
||||||
import nucleus.factory.RequiresPresenter
|
import nucleus.factory.RequiresPresenter
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.subscriptions.CompositeSubscription
|
import rx.subscriptions.CompositeSubscription
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
@RequiresPresenter(ReaderPresenter::class)
|
@RequiresPresenter(ReaderPresenter::class)
|
||||||
class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
||||||
@ -69,6 +72,8 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
|
|
||||||
private var customBrightnessSubscription: Subscription? = null
|
private var customBrightnessSubscription: Subscription? = null
|
||||||
|
|
||||||
|
private var customFilterColorSubscription: Subscription? = null
|
||||||
|
|
||||||
var readerTheme: Int = 0
|
var readerTheme: Int = 0
|
||||||
private set
|
private set
|
||||||
|
|
||||||
@ -105,7 +110,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
|
|
||||||
setMenuVisibility(menuVisible)
|
setMenuVisibility(menuVisible)
|
||||||
|
|
||||||
maxBitmapSize = GLUtil.getMaxTextureSize()
|
maxBitmapSize = Math.min(2048, GLUtil.getMaxTextureSize())
|
||||||
|
|
||||||
left_chapter.setOnClickListener {
|
left_chapter.setOnClickListener {
|
||||||
if (viewer != null) {
|
if (viewer != null) {
|
||||||
@ -139,6 +144,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.action_settings -> ReaderSettingsDialog().show(supportFragmentManager, "settings")
|
R.id.action_settings -> ReaderSettingsDialog().show(supportFragmentManager, "settings")
|
||||||
|
R.id.action_custom_filter -> ReaderCustomFilterDialog().show(supportFragmentManager, "filter")
|
||||||
else -> return super.onOptionsItemSelected(item)
|
else -> return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@ -149,6 +155,13 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||||
|
super.onWindowFocusChanged(hasFocus)
|
||||||
|
if (hasFocus) {
|
||||||
|
setMenuVisibility(menuVisible, animate = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
val chapterToUpdate = presenter.getMangaSyncChapterToUpdate()
|
val chapterToUpdate = presenter.getMangaSyncChapterToUpdate()
|
||||||
|
|
||||||
@ -206,7 +219,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onChapterError(error: Throwable) {
|
fun onChapterError(error: Throwable) {
|
||||||
Timber.e(error, error.message)
|
Timber.e(error)
|
||||||
finish()
|
finish()
|
||||||
toast(error.message)
|
toast(error.message)
|
||||||
}
|
}
|
||||||
@ -301,15 +314,18 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
return fragment
|
return fragment
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onPageChanged(currentPageIndex: Int, totalPages: Int) {
|
fun onPageChanged(page: Page) {
|
||||||
val page = currentPageIndex + 1
|
presenter.onPageChanged(page)
|
||||||
page_number.text = "$page/$totalPages"
|
|
||||||
|
val pageNumber = page.pageNumber + 1
|
||||||
|
val pageCount = page.chapter.pages!!.size
|
||||||
|
page_number.text = "$pageNumber/$pageCount"
|
||||||
if (page_seekbar.rotation != 180f) {
|
if (page_seekbar.rotation != 180f) {
|
||||||
left_page_text.text = "$page"
|
left_page_text.text = "$pageNumber"
|
||||||
} else {
|
} else {
|
||||||
right_page_text.text = "$page"
|
right_page_text.text = "$pageNumber"
|
||||||
}
|
}
|
||||||
page_seekbar.progress = currentPageIndex
|
page_seekbar.progress = page.pageNumber
|
||||||
}
|
}
|
||||||
|
|
||||||
fun gotoPageInCurrentChapter(pageIndex: Int) {
|
fun gotoPageInCurrentChapter(pageIndex: Int) {
|
||||||
@ -319,7 +335,6 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
val requestedPage = activePage.chapter.pages!![pageIndex]
|
val requestedPage = activePage.chapter.pages!![pageIndex]
|
||||||
it.setActivePage(requestedPage)
|
it.setActivePage(requestedPage)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -344,9 +359,9 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
reader_menu_bottom.setOnTouchListener { v, event -> true }
|
reader_menu_bottom.setOnTouchListener { v, event -> true }
|
||||||
|
|
||||||
page_seekbar.setOnSeekBarChangeListener(object : SimpleSeekBarListener() {
|
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) {
|
if (fromUser) {
|
||||||
gotoPageInCurrentChapter(progress)
|
gotoPageInCurrentChapter(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -368,6 +383,9 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
subscriptions += preferences.customBrightness().asObservable()
|
subscriptions += preferences.customBrightness().asObservable()
|
||||||
.subscribe { setCustomBrightness(it) }
|
.subscribe { setCustomBrightness(it) }
|
||||||
|
|
||||||
|
subscriptions += preferences.colorFilter().asObservable()
|
||||||
|
.subscribe { setColorFilter(it) }
|
||||||
|
|
||||||
subscriptions += preferences.readerTheme().asObservable()
|
subscriptions += preferences.readerTheme().asObservable()
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.subscribe { applyTheme(it) }
|
.subscribe { applyTheme(it) }
|
||||||
@ -414,6 +432,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
private fun setCustomBrightness(enabled: Boolean) {
|
private fun setCustomBrightness(enabled: Boolean) {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
customBrightnessSubscription = preferences.customBrightnessValue().asObservable()
|
customBrightnessSubscription = preferences.customBrightnessValue().asObservable()
|
||||||
|
.sample(100, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
|
||||||
.subscribe { setCustomBrightnessValue(it) }
|
.subscribe { setCustomBrightnessValue(it) }
|
||||||
|
|
||||||
subscriptions.add(customBrightnessSubscription)
|
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].
|
* 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 -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) {
|
private fun applyTheme(theme: Int) {
|
||||||
readerTheme = theme
|
readerTheme = theme
|
||||||
val rootView = window.decorView.rootView
|
val rootView = window.decorView.rootView
|
||||||
@ -463,12 +500,13 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setMenuVisibility(visible: Boolean) {
|
private fun setMenuVisibility(visible: Boolean, animate: Boolean = true) {
|
||||||
menuVisible = visible
|
menuVisible = visible
|
||||||
if (visible) {
|
if (visible) {
|
||||||
systemUi?.show()
|
systemUi?.show()
|
||||||
reader_menu.visibility = View.VISIBLE
|
reader_menu.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
if (animate) {
|
||||||
val toolbarAnimation = AnimationUtils.loadAnimation(this, R.anim.enter_from_top)
|
val toolbarAnimation = AnimationUtils.loadAnimation(this, R.anim.enter_from_top)
|
||||||
toolbarAnimation.setAnimationListener(object : SimpleAnimationListener() {
|
toolbarAnimation.setAnimationListener(object : SimpleAnimationListener() {
|
||||||
override fun onAnimationStart(animation: Animation) {
|
override fun onAnimationStart(animation: Animation) {
|
||||||
@ -482,8 +520,11 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
|
|
||||||
val bottomMenuAnimation = AnimationUtils.loadAnimation(this, R.anim.enter_from_bottom)
|
val bottomMenuAnimation = AnimationUtils.loadAnimation(this, R.anim.enter_from_bottom)
|
||||||
reader_menu_bottom.startAnimation(bottomMenuAnimation)
|
reader_menu_bottom.startAnimation(bottomMenuAnimation)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
systemUi?.hide()
|
systemUi?.hide()
|
||||||
|
|
||||||
|
if (animate) {
|
||||||
val toolbarAnimation = AnimationUtils.loadAnimation(this, R.anim.exit_to_top)
|
val toolbarAnimation = AnimationUtils.loadAnimation(this, R.anim.exit_to_top)
|
||||||
toolbarAnimation.setAnimationListener(object : SimpleAnimationListener() {
|
toolbarAnimation.setAnimationListener(object : SimpleAnimationListener() {
|
||||||
override fun onAnimationEnd(animation: Animation) {
|
override fun onAnimationEnd(animation: Animation) {
|
||||||
@ -496,5 +537,6 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
reader_menu_bottom.startAnimation(bottomMenuAnimation)
|
reader_menu_bottom.startAnimation(bottomMenuAnimation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -21,6 +21,7 @@ import rx.Observable
|
|||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@ -228,12 +229,14 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
|||||||
* strategy set for the manga.
|
* strategy set for the manga.
|
||||||
*
|
*
|
||||||
* @param chapter the current active chapter.
|
* @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 -> {
|
Manga.SORTING_SOURCE -> {
|
||||||
val currChapterIndex = chapterList.indexOfFirst { chapter.id == it.id }
|
val currChapterIndex = chapterList.indexOfFirst { chapter.id == it.id }
|
||||||
val nextChapter = chapterList.getOrNull(currChapterIndex + 1)
|
val nextChapter = chapterList.getOrNull(currChapterIndex + nextChapterAmount)
|
||||||
val prevChapter = chapterList.getOrNull(currChapterIndex - 1)
|
val prevChapter = chapterList.getOrNull(currChapterIndex - previousChapterAmount)
|
||||||
Pair(prevChapter, nextChapter)
|
Pair(prevChapter, nextChapter)
|
||||||
}
|
}
|
||||||
Manga.SORTING_NUMBER -> {
|
Manga.SORTING_NUMBER -> {
|
||||||
@ -241,18 +244,18 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
|||||||
val chapterNumber = chapter.chapter_number
|
val chapterNumber = chapter.chapter_number
|
||||||
|
|
||||||
var prevChapter: ReaderChapter? = null
|
var prevChapter: ReaderChapter? = null
|
||||||
for (i in (currChapterIndex - 1) downTo 0) {
|
for (i in (currChapterIndex - previousChapterAmount) downTo 0) {
|
||||||
val c = chapterList[i]
|
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
|
prevChapter = c
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var nextChapter: ReaderChapter? = null
|
var nextChapter: ReaderChapter? = null
|
||||||
for (i in (currChapterIndex + 1) until chapterList.size) {
|
for (i in (currChapterIndex + nextChapterAmount) until chapterList.size) {
|
||||||
val c = chapterList[i]
|
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
|
nextChapter = c
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -344,39 +347,42 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
|||||||
fun onChapterLeft() {
|
fun onChapterLeft() {
|
||||||
// Reference these locally because they are needed later from another thread.
|
// Reference these locally because they are needed later from another thread.
|
||||||
val chapter = chapter
|
val chapter = chapter
|
||||||
val prevChapter = prevChapter
|
|
||||||
|
|
||||||
val pages = chapter.pages ?: return
|
val pages = chapter.pages ?: return
|
||||||
|
|
||||||
Observable
|
Observable.fromCallable {
|
||||||
.fromCallable {
|
|
||||||
// Chapters with 1 page don't trigger page changes, so mark them as read.
|
// Chapters with 1 page don't trigger page changes, so mark them as read.
|
||||||
if (pages.size == 1) {
|
if (pages.size == 1) {
|
||||||
chapter.read = true
|
chapter.read = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache current page list progress for online chapters to allow a faster reopen
|
||||||
if (!chapter.isDownloaded) {
|
if (!chapter.isDownloaded) {
|
||||||
source.let { if (it is OnlineSource) it.savePageList(chapter, pages) }
|
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) {
|
if (chapter.read) {
|
||||||
// Check if remove after read is selected by user
|
val removeAfterReadSlots = prefs.removeAfterReadSlots()
|
||||||
if (prefs.removeAfterRead()) {
|
when (removeAfterReadSlots) {
|
||||||
if (prefs.removeAfterReadPrevious() ) {
|
// Setting disabled
|
||||||
if (prevChapter != null) {
|
-1 -> { /**Empty function**/ }
|
||||||
deleteChapter(prevChapter, manga)
|
// Remove current read chapter
|
||||||
}
|
0 -> deleteChapter(chapter, manga)
|
||||||
} else {
|
// Remove previous chapter specified by user in settings.
|
||||||
deleteChapter(chapter, manga)
|
else -> getAdjacentChaptersStrategy(chapter, removeAfterReadSlots)
|
||||||
}
|
.first?.let { deleteChapter(it, manga) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
db.updateChapterProgress(chapter).executeAsBlocking()
|
db.updateChapterProgress(chapter).executeAsBlocking()
|
||||||
|
|
||||||
|
try {
|
||||||
val history = History.create(chapter).apply { last_read = Date().time }
|
val history = History.create(chapter).apply { last_read = Date().time }
|
||||||
db.updateHistoryLastRead(history).executeAsBlocking()
|
db.updateHistoryLastRead(history).executeAsBlocking()
|
||||||
|
} catch (error: Exception) {
|
||||||
|
// TODO find out why it crashes
|
||||||
|
Timber.e(error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.subscribe()
|
.subscribe()
|
||||||
|
@ -11,7 +11,6 @@ import eu.kanade.tachiyomi.data.preference.getOrDefault
|
|||||||
import eu.kanade.tachiyomi.util.plusAssign
|
import eu.kanade.tachiyomi.util.plusAssign
|
||||||
import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener
|
import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener
|
||||||
import kotlinx.android.synthetic.main.dialog_reader_settings.view.*
|
import kotlinx.android.synthetic.main.dialog_reader_settings.view.*
|
||||||
import org.adw.library.widgets.discreteseekbar.DiscreteSeekBar
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.subscriptions.CompositeSubscription
|
import rx.subscriptions.CompositeSubscription
|
||||||
@ -84,24 +83,6 @@ class ReaderSettingsDialog : DialogFragment() {
|
|||||||
fullscreen.setOnCheckedChangeListener { v, isChecked ->
|
fullscreen.setOnCheckedChangeListener { v, isChecked ->
|
||||||
preferences.fullscreen().set(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() {
|
override fun onDestroyView() {
|
||||||
|
@ -66,16 +66,6 @@ abstract class BaseReader : BaseFragment() {
|
|||||||
*/
|
*/
|
||||||
private var hasRequestedNextChapter: Boolean = false
|
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.
|
* Returns the active page.
|
||||||
*/
|
*/
|
||||||
@ -91,11 +81,13 @@ abstract class BaseReader : BaseFragment() {
|
|||||||
fun onPageChanged(position: Int) {
|
fun onPageChanged(position: Int) {
|
||||||
val oldPage = pages[currentPage]
|
val oldPage = pages[currentPage]
|
||||||
val newPage = pages[position]
|
val newPage = pages[position]
|
||||||
readerActivity.presenter.onPageChanged(newPage)
|
|
||||||
|
|
||||||
val oldChapter = oldPage.chapter
|
val oldChapter = oldPage.chapter
|
||||||
val newChapter = newPage.chapter
|
val newChapter = newPage.chapter
|
||||||
|
|
||||||
|
// Update page indicator and seekbar
|
||||||
|
readerActivity.onPageChanged(newPage)
|
||||||
|
|
||||||
// Active chapter has changed.
|
// Active chapter has changed.
|
||||||
if (oldChapter.id != newChapter.id) {
|
if (oldChapter.id != newChapter.id) {
|
||||||
readerActivity.onEnterChapter(newPage.chapter, newPage.pageNumber)
|
readerActivity.onEnterChapter(newPage.chapter, newPage.pageNumber)
|
||||||
@ -108,7 +100,6 @@ abstract class BaseReader : BaseFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
currentPage = position
|
currentPage = position
|
||||||
updatePageNumber()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,23 +1,24 @@
|
|||||||
package eu.kanade.tachiyomi.ui.reader.viewer.pager
|
package eu.kanade.tachiyomi.ui.reader.viewer.pager
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.graphics.PointF
|
import android.graphics.PointF
|
||||||
import android.os.Bundle
|
import android.util.AttributeSet
|
||||||
import android.support.v4.content.ContextCompat
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.widget.FrameLayout
|
||||||
import com.davemorrissey.labs.subscaleview.ImageSource
|
import com.davemorrissey.labs.subscaleview.ImageSource
|
||||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
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.ReaderActivity
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.base.PageDecodeErrorLayout
|
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.horizontal.RightToLeftReader
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.vertical.VerticalReader
|
import eu.kanade.tachiyomi.ui.reader.viewer.pager.vertical.VerticalReader
|
||||||
import kotlinx.android.synthetic.main.chapter_image.*
|
import kotlinx.android.synthetic.main.chapter_image.view.*
|
||||||
import kotlinx.android.synthetic.main.item_pager_reader.*
|
import kotlinx.android.synthetic.main.item_pager_reader.view.*
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
@ -25,41 +26,15 @@ import rx.subjects.PublishSubject
|
|||||||
import rx.subjects.SerializedSubject
|
import rx.subjects.SerializedSubject
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
|
||||||
|
|
||||||
/**
|
class PageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null)
|
||||||
* Fragment for a single page of the ViewPager reader.
|
: FrameLayout(context, attrs) {
|
||||||
* 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page of a chapter.
|
* Page of a chapter.
|
||||||
*/
|
*/
|
||||||
var page: Page? = null
|
var page: Page? = null
|
||||||
set(value) {
|
private set
|
||||||
field = value
|
|
||||||
// Observe status if the view is initialized
|
|
||||||
if (view != null) {
|
|
||||||
observeStatus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Position of the fragment in the adapter.
|
|
||||||
*/
|
|
||||||
var position = -1
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscription for progress changes of the page.
|
* Subscription for progress changes of the page.
|
||||||
@ -71,47 +46,35 @@ class PagerReaderFragment : BaseFragment() {
|
|||||||
*/
|
*/
|
||||||
private var statusSubscription: Subscription? = null
|
private var statusSubscription: Subscription? = null
|
||||||
|
|
||||||
/**
|
fun initialize(reader: PagerReader, page: Page?) {
|
||||||
* Text color for black theme.
|
val activity = reader.activity as ReaderActivity
|
||||||
*/
|
|
||||||
private val whiteColor by lazy { ContextCompat.getColor(context, R.color.textColorSecondaryDark) }
|
|
||||||
|
|
||||||
/**
|
when (activity.readerTheme) {
|
||||||
* Text color for white theme.
|
ReaderActivity.BLACK_THEME -> progress_text.setTextColor(reader.whiteColor)
|
||||||
*/
|
ReaderActivity.WHITE_THEME -> progress_text.setTextColor(reader.blackColor)
|
||||||
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 (reader is RightToLeftReader) {
|
||||||
if (readerActivity.readerTheme == ReaderActivity.BLACK_THEME) {
|
rotation = -180f
|
||||||
progress_text.setTextColor(whiteColor)
|
|
||||||
} else {
|
|
||||||
progress_text.setTextColor(blackColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pagerReader is RightToLeftReader) {
|
|
||||||
view.rotation = -180f
|
|
||||||
}
|
}
|
||||||
|
|
||||||
with(image_view) {
|
with(image_view) {
|
||||||
setMaxBitmapDimensions(readerActivity.maxBitmapSize)
|
setMaxBitmapDimensions((reader.activity as ReaderActivity).maxBitmapSize)
|
||||||
setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED)
|
setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED)
|
||||||
setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE)
|
setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE)
|
||||||
setMinimumScaleType(pagerReader.scaleType)
|
setMinimumScaleType(reader.scaleType)
|
||||||
setMinimumDpi(50)
|
setMinimumDpi(90)
|
||||||
setRegionDecoderClass(pagerReader.regionDecoderClass)
|
setMinimumTileDpi(180)
|
||||||
setBitmapDecoderClass(pagerReader.bitmapDecoderClass)
|
setRegionDecoderClass(reader.regionDecoderClass)
|
||||||
setVerticalScrollingParent(pagerReader is VerticalReader)
|
setBitmapDecoderClass(reader.bitmapDecoderClass)
|
||||||
setOnTouchListener { v, motionEvent -> pagerReader.gestureDetector.onTouchEvent(motionEvent) }
|
setVerticalScrollingParent(reader is VerticalReader)
|
||||||
|
setOnTouchListener { v, motionEvent -> reader.gestureDetector.onTouchEvent(motionEvent) }
|
||||||
setOnImageEventListener(object : SubsamplingScaleImageView.DefaultOnImageEventListener() {
|
setOnImageEventListener(object : SubsamplingScaleImageView.DefaultOnImageEventListener() {
|
||||||
override fun onReady() {
|
override fun onReady() {
|
||||||
when (pagerReader.zoomType) {
|
when (reader.zoomType) {
|
||||||
PagerReader.ALIGN_LEFT -> setScaleAndCenter(scale, PointF(0f, 0f))
|
ALIGN_LEFT -> setScaleAndCenter(scale, PointF(0f, 0f))
|
||||||
PagerReader.ALIGN_RIGHT -> setScaleAndCenter(scale, PointF(sWidth.toFloat(), 0f))
|
ALIGN_RIGHT -> setScaleAndCenter(scale, PointF(sWidth.toFloat(), 0f))
|
||||||
PagerReader.ALIGN_CENTER -> {
|
ALIGN_CENTER -> {
|
||||||
val newCenter = center
|
val newCenter = center
|
||||||
newCenter.y = 0f
|
newCenter.y = 0f
|
||||||
setScaleAndCenter(scale, newCenter)
|
setScaleAndCenter(scale, newCenter)
|
||||||
@ -120,27 +83,34 @@ class PagerReaderFragment : BaseFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onImageLoadError(e: Exception) {
|
override fun onImageLoadError(e: Exception) {
|
||||||
onImageDecodeError()
|
onImageDecodeError(activity)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
retry_button.setOnTouchListener { v, event ->
|
retry_button.setOnTouchListener { v, event ->
|
||||||
if (event.action == MotionEvent.ACTION_UP) {
|
if (event.action == MotionEvent.ACTION_UP) {
|
||||||
readerActivity.presenter.retryPage(page)
|
activity.presenter.retryPage(page)
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (page != null) {
|
||||||
|
this.page = page
|
||||||
observeStatus()
|
observeStatus()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
fun cleanup() {
|
||||||
unsubscribeProgress()
|
unsubscribeProgress()
|
||||||
unsubscribeStatus()
|
unsubscribeStatus()
|
||||||
image_view.setOnTouchListener(null)
|
image_view.setOnTouchListener(null)
|
||||||
image_view.setOnImageEventListener(null)
|
image_view.setOnImageEventListener(null)
|
||||||
super.onDestroyView()
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromWindow() {
|
||||||
|
cleanup()
|
||||||
|
super.onDetachedFromWindow()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -149,33 +119,31 @@ class PagerReaderFragment : BaseFragment() {
|
|||||||
* @see processStatus
|
* @see processStatus
|
||||||
*/
|
*/
|
||||||
private fun observeStatus() {
|
private fun observeStatus() {
|
||||||
page?.let { page ->
|
statusSubscription?.unsubscribe()
|
||||||
|
val page = page ?: return
|
||||||
|
|
||||||
val statusSubject = SerializedSubject(PublishSubject.create<Int>())
|
val statusSubject = SerializedSubject(PublishSubject.create<Int>())
|
||||||
page.setStatusSubject(statusSubject)
|
page.setStatusSubject(statusSubject)
|
||||||
|
|
||||||
statusSubscription?.unsubscribe()
|
|
||||||
statusSubscription = statusSubject.startWith(page.status)
|
statusSubscription = statusSubject.startWith(page.status)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe { processStatus(it) }
|
.subscribe { processStatus(it) }
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observes the progress of the page and updates view.
|
* Observes the progress of the page and updates view.
|
||||||
*/
|
*/
|
||||||
private fun observeProgress() {
|
private fun observeProgress() {
|
||||||
val currentValue = AtomicInteger(-1)
|
|
||||||
|
|
||||||
progressSubscription?.unsubscribe()
|
progressSubscription?.unsubscribe()
|
||||||
|
val page = page ?: return
|
||||||
|
|
||||||
progressSubscription = Observable.interval(100, TimeUnit.MILLISECONDS)
|
progressSubscription = Observable.interval(100, TimeUnit.MILLISECONDS)
|
||||||
|
.map { page.progress }
|
||||||
|
.distinctUntilChanged()
|
||||||
.onBackpressureLatest()
|
.onBackpressureLatest()
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe {
|
.subscribe { progress ->
|
||||||
// Refresh UI only if progress change
|
progress_text.text = context.getString(R.string.download_progress, progress)
|
||||||
if (page?.progress != currentValue.get()) {
|
|
||||||
currentValue.set(page?.progress ?: 0)
|
|
||||||
progress_text.text = getString(R.string.download_progress, currentValue.get())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -269,27 +237,13 @@ class PagerReaderFragment : BaseFragment() {
|
|||||||
/**
|
/**
|
||||||
* Called when an image fails to decode.
|
* Called when an image fails to decode.
|
||||||
*/
|
*/
|
||||||
private fun onImageDecodeError() {
|
private fun onImageDecodeError(activity: ReaderActivity) {
|
||||||
val view = view as? ViewGroup ?: return
|
|
||||||
|
|
||||||
page?.let { page ->
|
page?.let { page ->
|
||||||
val errorLayout = PageDecodeErrorLayout(context, page, readerActivity.readerTheme,
|
val errorLayout = PageDecodeErrorLayout(context, page, activity.readerTheme,
|
||||||
{ readerActivity.presenter.retryPage(page) })
|
{ 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
|
|
||||||
|
|
||||||
}
|
}
|
@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.ui.reader.viewer.pager
|
package eu.kanade.tachiyomi.ui.reader.viewer.pager
|
||||||
|
|
||||||
|
import android.support.v4.content.ContextCompat
|
||||||
import android.view.GestureDetector
|
import android.view.GestureDetector
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@ -90,13 +91,23 @@ abstract class PagerReader : BaseReader() {
|
|||||||
var zoomType = 1
|
var zoomType = 1
|
||||||
private set
|
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.
|
* Initializes the pager.
|
||||||
*
|
*
|
||||||
* @param pager the pager to initialize.
|
* @param pager the pager to initialize.
|
||||||
*/
|
*/
|
||||||
protected fun initializePager(pager: Pager) {
|
protected fun initializePager(pager: Pager) {
|
||||||
adapter = PagerReaderAdapter(childFragmentManager)
|
adapter = PagerReaderAdapter(this)
|
||||||
|
|
||||||
this.pager = pager.apply {
|
this.pager = pager.apply {
|
||||||
setLayoutParams(ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT))
|
setLayoutParams(ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT))
|
||||||
@ -161,6 +172,7 @@ abstract class PagerReader : BaseReader() {
|
|||||||
protected fun createGestureDetector(): GestureDetector {
|
protected fun createGestureDetector(): GestureDetector {
|
||||||
return GestureDetector(activity, object : GestureDetector.SimpleOnGestureListener() {
|
return GestureDetector(activity, object : GestureDetector.SimpleOnGestureListener() {
|
||||||
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
|
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
|
||||||
|
if (isAdded) {
|
||||||
val positionX = e.x
|
val positionX = e.x
|
||||||
|
|
||||||
if (positionX < pager.width * LEFT_REGION) {
|
if (positionX < pager.width * LEFT_REGION) {
|
||||||
@ -170,6 +182,7 @@ abstract class PagerReader : BaseReader() {
|
|||||||
} else {
|
} else {
|
||||||
readerActivity.toggleMenu()
|
readerActivity.toggleMenu()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -208,8 +221,11 @@ abstract class PagerReader : BaseReader() {
|
|||||||
protected fun setPagesOnAdapter() {
|
protected fun setPagesOnAdapter() {
|
||||||
if (pages.isNotEmpty()) {
|
if (pages.isNotEmpty()) {
|
||||||
adapter.pages = pages
|
adapter.pages = pages
|
||||||
|
if (currentPage == pager.currentItem) {
|
||||||
|
onPageChanged(currentPage)
|
||||||
|
} else {
|
||||||
setActivePage(currentPage)
|
setActivePage(currentPage)
|
||||||
updatePageNumber()
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,19 +1,16 @@
|
|||||||
package eu.kanade.tachiyomi.ui.reader.viewer.pager
|
package eu.kanade.tachiyomi.ui.reader.viewer.pager
|
||||||
|
|
||||||
import android.support.v4.app.Fragment
|
import android.view.View
|
||||||
import android.support.v4.app.FragmentManager
|
|
||||||
import android.support.v4.app.FragmentStatePagerAdapter
|
|
||||||
import android.support.v4.view.PagerAdapter
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
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.
|
* 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.
|
* Pages stored in the adapter.
|
||||||
@ -24,6 +21,12 @@ class PagerReaderAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm) {
|
|||||||
notifyDataSetChanged()
|
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.
|
* Returns the number of pages.
|
||||||
*
|
*
|
||||||
@ -33,46 +36,4 @@ class PagerReaderAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm) {
|
|||||||
return pages?.size ?: 0
|
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,8 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
|
|||||||
setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED)
|
setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED)
|
||||||
setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE)
|
setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE)
|
||||||
setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_FIT_WIDTH)
|
setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_FIT_WIDTH)
|
||||||
maxScale = 10f
|
setMinimumDpi(90)
|
||||||
|
setMinimumTileDpi(180)
|
||||||
setRegionDecoderClass(webtoonReader.regionDecoderClass)
|
setRegionDecoderClass(webtoonReader.regionDecoderClass)
|
||||||
setBitmapDecoderClass(webtoonReader.bitmapDecoderClass)
|
setBitmapDecoderClass(webtoonReader.bitmapDecoderClass)
|
||||||
setVerticalScrollingParent(true)
|
setVerticalScrollingParent(true)
|
||||||
|
@ -85,9 +85,9 @@ class WebtoonReader : BaseReader() {
|
|||||||
recycler.adapter = adapter
|
recycler.adapter = adapter
|
||||||
recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
|
override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
|
||||||
val page = layoutManager.findLastVisibleItemPosition()
|
val index = layoutManager.findLastVisibleItemPosition()
|
||||||
if (page != currentPage) {
|
if (index != currentPage) {
|
||||||
onPageChanged(page)
|
pages.getOrNull(index)?.let { onPageChanged(index) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -127,6 +127,7 @@ class WebtoonReader : BaseReader() {
|
|||||||
protected fun createGestureDetector(): GestureDetector {
|
protected fun createGestureDetector(): GestureDetector {
|
||||||
return GestureDetector(context, object : SimpleOnGestureListener() {
|
return GestureDetector(context, object : SimpleOnGestureListener() {
|
||||||
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
|
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
|
||||||
|
if (isAdded) {
|
||||||
val positionX = e.x
|
val positionX = e.x
|
||||||
|
|
||||||
if (positionX < recycler.width * LEFT_REGION) {
|
if (positionX < recycler.width * LEFT_REGION) {
|
||||||
@ -136,6 +137,7 @@ class WebtoonReader : BaseReader() {
|
|||||||
} else {
|
} else {
|
||||||
readerActivity.toggleMenu()
|
readerActivity.toggleMenu()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -148,8 +150,7 @@ class WebtoonReader : BaseReader() {
|
|||||||
* @param currentPage the initial page to display.
|
* @param currentPage the initial page to display.
|
||||||
*/
|
*/
|
||||||
override fun onChapterSet(chapter: ReaderChapter, currentPage: Page) {
|
override fun onChapterSet(chapter: ReaderChapter, currentPage: Page) {
|
||||||
// Restoring current page is not supported. It's getting weird scrolling jumps
|
this.currentPage = currentPage.pageNumber
|
||||||
// this.currentPage = currentPage;
|
|
||||||
|
|
||||||
// Make sure the view is already initialized.
|
// Make sure the view is already initialized.
|
||||||
if (view != null) {
|
if (view != null) {
|
||||||
@ -177,7 +178,7 @@ class WebtoonReader : BaseReader() {
|
|||||||
if (pages.isNotEmpty()) {
|
if (pages.isNotEmpty()) {
|
||||||
adapter.pages = pages
|
adapter.pages = pages
|
||||||
recycler.adapter = adapter
|
recycler.adapter = adapter
|
||||||
updatePageNumber()
|
onPageChanged(currentPage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -253,7 +253,7 @@ class RecentChaptersFragment
|
|||||||
*/
|
*/
|
||||||
fun onChaptersDeletedError(error: Throwable) {
|
fun onChaptersDeletedError(error: Throwable) {
|
||||||
dismissDeletingDialog()
|
dismissDeletingDialog()
|
||||||
Timber.e(error, error.message)
|
Timber.e(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -71,7 +71,7 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
|
|||||||
// Set chapter status
|
// Set chapter status
|
||||||
view.onChapterStatusChange(download)
|
view.onChapterStatusChange(download)
|
||||||
},
|
},
|
||||||
{ view, error -> Timber.e(error.cause, error.message) }
|
{ view, error -> Timber.e(error) }
|
||||||
)
|
)
|
||||||
|
|
||||||
if (savedState == null) {
|
if (savedState == null) {
|
||||||
|
@ -115,7 +115,7 @@ class RecentlyReadPresenter : BasePresenter<RecentlyReadFragment>() {
|
|||||||
.subscribeFirst({ view, chapter ->
|
.subscribeFirst({ view, chapter ->
|
||||||
view.onOpenNextChapter(chapter, manga)
|
view.onOpenNextChapter(chapter, manga)
|
||||||
}, { view, error ->
|
}, { view, error ->
|
||||||
Timber.e(error, error.message)
|
Timber.e(error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,37 +1,27 @@
|
|||||||
package eu.kanade.tachiyomi.ui.setting
|
package eu.kanade.tachiyomi.ui.setting
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.v7.preference.SwitchPreferenceCompat
|
|
||||||
import android.support.v7.preference.XpPreferenceFragment
|
import android.support.v7.preference.XpPreferenceFragment
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.updater.GithubUpdateChecker
|
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 eu.kanade.tachiyomi.util.toast
|
||||||
|
import net.xpece.android.support.preference.SwitchPreference
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
|
import timber.log.Timber
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.text.ParseException
|
import java.text.ParseException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class SettingsAboutFragment : SettingsFragment() {
|
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 {
|
companion object {
|
||||||
fun newInstance(rootKey: String): SettingsAboutFragment {
|
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?) {
|
override fun onViewCreated(view: View, savedState: Bundle?) {
|
||||||
super.onViewCreated(view, savedState)
|
super.onViewCreated(view, savedState)
|
||||||
|
|
||||||
@ -48,15 +50,30 @@ class SettingsAboutFragment : SettingsFragment() {
|
|||||||
val buildTime = findPreference(getString(R.string.pref_build_time))
|
val buildTime = findPreference(getString(R.string.pref_build_time))
|
||||||
findPreference("acra.enable").isEnabled = false;
|
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.
|
if (!BuildConfig.DEBUG && BuildConfig.INCLUDE_UPDATER) {
|
||||||
// automaticUpdateToggle.isEnabled = true
|
//Set onClickListener to check for new version
|
||||||
// automaticUpdateToggle.setOnPreferenceChangeListener { preference, any ->
|
version.setOnPreferenceClickListener {
|
||||||
// val status = any as Boolean
|
checkVersion()
|
||||||
// UpdateDownloaderAlarm.startAlarm(activity, 12, status)
|
true
|
||||||
// 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()
|
buildTime.summary = getFormattedBuildTime()
|
||||||
}
|
}
|
||||||
@ -88,36 +105,35 @@ class SettingsAboutFragment : SettingsFragment() {
|
|||||||
private fun checkVersion() {
|
private fun checkVersion() {
|
||||||
releaseSubscription?.unsubscribe()
|
releaseSubscription?.unsubscribe()
|
||||||
|
|
||||||
releaseSubscription = updateChecker.checkForApplicationUpdate()
|
context.toast(R.string.update_check_look_for_updates)
|
||||||
|
|
||||||
|
releaseSubscription = updateChecker.checkForUpdate()
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe({ release ->
|
.subscribe({ result ->
|
||||||
//Get version of latest release
|
when (result) {
|
||||||
var newVersion = release.version
|
is GithubUpdateResult.NewUpdate -> {
|
||||||
newVersion = newVersion.replace("[^\\d.]".toRegex(), "")
|
val body = result.release.changeLog
|
||||||
|
val url = result.release.downloadLink
|
||||||
|
|
||||||
//Check if latest version is different from current version
|
// Create confirmation window
|
||||||
if (newVersion != BuildConfig.VERSION_NAME) {
|
MaterialDialog.Builder(context)
|
||||||
val downloadLink = release.downloadLink
|
|
||||||
val body = release.changeLog
|
|
||||||
|
|
||||||
//Create confirmation window
|
|
||||||
MaterialDialog.Builder(activity)
|
|
||||||
.title(R.string.update_check_title)
|
.title(R.string.update_check_title)
|
||||||
.content(body)
|
.content(body)
|
||||||
.positiveText(getString(R.string.update_check_confirm))
|
.positiveText(getString(R.string.update_check_confirm))
|
||||||
.negativeText(getString(R.string.update_check_ignore))
|
.negativeText(getString(R.string.update_check_ignore))
|
||||||
.onPositive { dialog, which ->
|
.onPositive { dialog, which ->
|
||||||
// User output that download has started
|
|
||||||
activity.toast(R.string.update_check_download_started)
|
|
||||||
// Start download
|
// Start download
|
||||||
UpdateDownloader(activity.applicationContext).execute(downloadLink)
|
UpdateDownloaderService.downloadUpdate(context, url)
|
||||||
}.show()
|
|
||||||
} else {
|
|
||||||
activity.toast(R.string.update_check_no_new_updates)
|
|
||||||
}
|
}
|
||||||
}, {
|
.show()
|
||||||
it.printStackTrace()
|
}
|
||||||
|
is GithubUpdateResult.NoNewUpdate -> {
|
||||||
|
context.toast(R.string.update_check_no_new_updates)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, { error ->
|
||||||
|
Timber.e(error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
package eu.kanade.tachiyomi.ui.setting
|
package eu.kanade.tachiyomi.ui.setting
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.support.v7.preference.Preference
|
||||||
import android.support.v7.preference.XpPreferenceFragment
|
import android.support.v7.preference.XpPreferenceFragment
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
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.data.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.util.plusAssign
|
import eu.kanade.tachiyomi.util.plusAssign
|
||||||
import eu.kanade.tachiyomi.util.toast
|
import eu.kanade.tachiyomi.util.toast
|
||||||
@ -32,11 +34,13 @@ class SettingsAdvancedFragment : SettingsFragment() {
|
|||||||
|
|
||||||
private val db: DatabaseHelper by injectLazy()
|
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?) {
|
override fun onViewCreated(view: View, savedState: Bundle?) {
|
||||||
super.onViewCreated(view, savedState)
|
super.onViewCreated(view, savedState)
|
||||||
@ -57,6 +61,11 @@ class SettingsAdvancedFragment : SettingsFragment() {
|
|||||||
clearDatabase()
|
clearDatabase()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refreshMetadata.setOnPreferenceClickListener {
|
||||||
|
LibraryUpdateService.start(context, details = true)
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun clearChapterCache() {
|
private fun clearChapterCache() {
|
||||||
|
@ -5,6 +5,7 @@ import android.content.Intent
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.support.v4.content.ContextCompat
|
import android.support.v4.content.ContextCompat
|
||||||
|
import android.support.v7.preference.Preference
|
||||||
import android.support.v7.preference.XpPreferenceFragment
|
import android.support.v7.preference.XpPreferenceFragment
|
||||||
import android.support.v7.widget.RecyclerView
|
import android.support.v7.widget.RecyclerView
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@ -36,7 +37,7 @@ class SettingsDownloadsFragment : SettingsFragment() {
|
|||||||
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
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?) {
|
override fun onViewCreated(view: View, savedState: Bundle?) {
|
||||||
super.onViewCreated(view, savedState)
|
super.onViewCreated(view, savedState)
|
||||||
|
@ -4,12 +4,13 @@ import android.os.Bundle
|
|||||||
import android.support.annotation.CallSuper
|
import android.support.annotation.CallSuper
|
||||||
import android.support.graphics.drawable.VectorDrawableCompat
|
import android.support.graphics.drawable.VectorDrawableCompat
|
||||||
import android.support.v4.content.ContextCompat
|
import android.support.v4.content.ContextCompat
|
||||||
|
import android.support.v7.preference.Preference
|
||||||
import android.support.v7.preference.XpPreferenceFragment
|
import android.support.v7.preference.XpPreferenceFragment
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.util.getResourceId
|
||||||
import net.xpece.android.support.preference.PreferenceIconHelper
|
import net.xpece.android.support.preference.PreferenceIconHelper
|
||||||
import net.xpece.android.support.preference.PreferenceScreenNavigationStrategy
|
import net.xpece.android.support.preference.PreferenceScreenNavigationStrategy
|
||||||
import net.xpece.android.support.preference.Util
|
|
||||||
import rx.subscriptions.CompositeSubscription
|
import rx.subscriptions.CompositeSubscription
|
||||||
|
|
||||||
open class SettingsFragment : XpPreferenceFragment() {
|
open class SettingsFragment : XpPreferenceFragment() {
|
||||||
@ -24,8 +25,8 @@ open class SettingsFragment : XpPreferenceFragment() {
|
|||||||
|
|
||||||
lateinit var subscriptions: CompositeSubscription
|
lateinit var subscriptions: CompositeSubscription
|
||||||
|
|
||||||
private val iconTint by lazy { ContextCompat.getColorStateList(
|
private val iconTint by lazy { ContextCompat.getColorStateList(context,
|
||||||
context, Util.resolveResourceId(context, R.attr.colorAccent, 0))
|
context.theme.getResourceId(R.attr.colorAccent, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
override final fun onCreatePreferences2(savedState: Bundle?, rootKey: String?) {
|
override final fun onCreatePreferences2(savedState: Bundle?, rootKey: String?) {
|
||||||
@ -60,6 +61,7 @@ open class SettingsFragment : XpPreferenceFragment() {
|
|||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
override fun onViewCreated(view: View, savedState: Bundle?) {
|
override fun onViewCreated(view: View, savedState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedState)
|
||||||
listView.isFocusable = false
|
listView.isFocusable = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,4 +86,8 @@ open class SettingsFragment : XpPreferenceFragment() {
|
|||||||
"about_screen" to R.drawable.ic_help_black_24dp
|
"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 }
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -6,7 +6,8 @@ import android.support.v7.preference.PreferenceFragmentCompat
|
|||||||
import android.support.v7.preference.XpPreferenceFragment
|
import android.support.v7.preference.XpPreferenceFragment
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import eu.kanade.tachiyomi.R
|
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.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.util.plusAssign
|
import eu.kanade.tachiyomi.util.plusAssign
|
||||||
import eu.kanade.tachiyomi.widget.preference.IntListPreference
|
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 eu.kanade.tachiyomi.widget.preference.SimpleDialogPreference
|
||||||
import net.xpece.android.support.preference.MultiSelectListPreference
|
import net.xpece.android.support.preference.MultiSelectListPreference
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class SettingsGeneralFragment : SettingsFragment(),
|
class SettingsGeneralFragment : SettingsFragment(),
|
||||||
@ -30,22 +32,17 @@ class SettingsGeneralFragment : SettingsFragment(),
|
|||||||
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
private val db: DatabaseHelper by injectLazy()
|
||||||
|
|
||||||
val columnsPreference by lazy {
|
val columnsPreference: SimpleDialogPreference by bindPref(R.string.pref_library_columns_dialog_key)
|
||||||
findPreference(getString(R.string.pref_library_columns_dialog_key)) as SimpleDialogPreference
|
|
||||||
}
|
|
||||||
|
|
||||||
val updateInterval by lazy {
|
val updateInterval: IntListPreference by bindPref(R.string.pref_library_update_interval_key)
|
||||||
findPreference(getString(R.string.pref_library_update_interval_key)) as IntListPreference
|
|
||||||
}
|
|
||||||
|
|
||||||
val updateRestriction by lazy {
|
val updateRestriction: MultiSelectListPreference by bindPref(R.string.pref_library_update_restriction_key)
|
||||||
findPreference(getString(R.string.pref_library_update_restriction_key)) as MultiSelectListPreference
|
|
||||||
}
|
|
||||||
|
|
||||||
val themePreference by lazy {
|
val themePreference: IntListPreference by bindPref(R.string.pref_theme_key)
|
||||||
findPreference(getString(R.string.pref_theme_key)) as IntListPreference
|
|
||||||
}
|
val categoryUpdate: MultiSelectListPreference by bindPref(R.string.pref_library_update_categories_key)
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedState: Bundle?) {
|
override fun onViewCreated(view: View, savedState: Bundle?) {
|
||||||
super.onViewCreated(view, savedState)
|
super.onViewCreated(view, savedState)
|
||||||
@ -60,10 +57,44 @@ class SettingsGeneralFragment : SettingsFragment(),
|
|||||||
.subscribe { updateColumnsSummary(it.first, it.second) }
|
.subscribe { updateColumnsSummary(it.first, it.second) }
|
||||||
|
|
||||||
updateInterval.setOnPreferenceChangeListener { preference, newValue ->
|
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
|
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 ->
|
themePreference.setOnPreferenceChangeListener { preference, newValue ->
|
||||||
(activity as SettingsActivity).parentFlags = SettingsActivity.FLAG_THEME_CHANGED
|
(activity as SettingsActivity).parentFlags = SettingsActivity.FLAG_THEME_CHANGED
|
||||||
activity.recreate()
|
activity.recreate()
|
||||||
|
@ -1,21 +1,19 @@
|
|||||||
package eu.kanade.tachiyomi.ui.setting
|
package eu.kanade.tachiyomi.ui.setting
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.v7.preference.Preference
|
|
||||||
import android.support.v7.preference.PreferenceGroup
|
|
||||||
import android.support.v7.preference.XpPreferenceFragment
|
import android.support.v7.preference.XpPreferenceFragment
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
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.SourceManager
|
||||||
import eu.kanade.tachiyomi.data.source.getLanguages
|
import eu.kanade.tachiyomi.data.source.getLanguages
|
||||||
import eu.kanade.tachiyomi.data.source.online.LoginSource
|
import eu.kanade.tachiyomi.widget.preference.LoginCheckBoxPreference
|
||||||
import eu.kanade.tachiyomi.util.plusAssign
|
|
||||||
import eu.kanade.tachiyomi.widget.preference.LoginPreference
|
|
||||||
import eu.kanade.tachiyomi.widget.preference.SourceLoginDialog
|
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
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class SettingsSourcesFragment : SettingsFragment() {
|
class SettingsSourcesFragment : SettingsFragment() {
|
||||||
@ -32,59 +30,105 @@ class SettingsSourcesFragment : SettingsFragment() {
|
|||||||
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
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 }
|
override fun setDivider(divider: Drawable?) {
|
||||||
|
super.setDivider(null)
|
||||||
val sourcesPref by lazy { findPreference("pref_sources") as PreferenceGroup }
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedState: Bundle?) {
|
override fun onViewCreated(view: View, savedState: Bundle?) {
|
||||||
super.onViewCreated(view, savedState)
|
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 langs = getLanguages().sortedBy { it.lang }
|
||||||
|
|
||||||
val entryKeys = langs.map { it.code }
|
// Order first by active languages, then inactive ones
|
||||||
languagesPref.entries = langs.map { it.lang }.toTypedArray()
|
val orderedLangs = langs.filter { it.code in activeLangsCodes } +
|
||||||
languagesPref.entryValues = entryKeys.toTypedArray()
|
langs.filterNot { it.code in activeLangsCodes }
|
||||||
languagesPref.values = preferences.enabledLanguages().getOrDefault()
|
|
||||||
|
|
||||||
subscriptions += preferences.enabledLanguages().asObservable()
|
orderedLangs.forEach { lang ->
|
||||||
.subscribe { languages ->
|
// Create a preference group and set initial state and change listener
|
||||||
sourcesPref.removeAll()
|
SwitchPreferenceCategory(context).apply {
|
||||||
|
preferenceScreen.addPreference(this)
|
||||||
val enabledSources = sourceManager.getOnlineSources()
|
title = lang.lang
|
||||||
.filter { it.lang.code in languages }
|
isPersistent = false
|
||||||
|
if (lang.code in activeLangsCodes) {
|
||||||
for (source in enabledSources.filterIsInstance(LoginSource::class.java)) {
|
setChecked(true)
|
||||||
val pref = createLoginSourceEntry(source)
|
addLanguageSources(this)
|
||||||
sourcesPref.addPreference(pref)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide category if it doesn't have any child
|
setOnPreferenceChangeListener { preference, any ->
|
||||||
sourcesPref.isVisible = sourcesPref.preferenceCount > 0
|
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 {
|
* Adds the source list for the given group (language).
|
||||||
key = preferences.keys.sourceUsername(source.id)
|
*
|
||||||
title = source.toString()
|
* @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)
|
||||||
|
|
||||||
setOnPreferenceClickListener {
|
|
||||||
val fragment = SourceLoginDialog.newInstance(source)
|
|
||||||
fragment.setTargetFragment(this@SettingsSourcesFragment, SOURCE_CHANGE_REQUEST)
|
|
||||||
fragment.show(fragmentManager, null)
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setOnLoginClickListener {
|
||||||
|
val fragment = SourceLoginDialog.newInstance(source)
|
||||||
|
fragment.setTargetFragment(this@SettingsSourcesFragment, SOURCE_CHANGE_REQUEST)
|
||||||
|
fragment.show(fragmentManager, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
group.addPreference(sourcePreference)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
if (requestCode == SOURCE_CHANGE_REQUEST) {
|
if (requestCode == SOURCE_CHANGE_REQUEST) {
|
||||||
val pref = findPreference(preferences.keys.sourceUsername(resultCode)) as? LoginPreference
|
val pref = findPreference(getSourceKey(resultCode)) as? LoginCheckBoxPreference
|
||||||
pref?.notifyChanged()
|
pref?.notifyChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getSourceKey(sourceId: Int): String {
|
||||||
|
return "source_$sourceId"
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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) }
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -2,18 +2,26 @@ package eu.kanade.tachiyomi.util
|
|||||||
|
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.support.annotation.AttrRes
|
||||||
import android.support.annotation.StringRes
|
import android.support.annotation.StringRes
|
||||||
|
|
||||||
fun Resources.Theme.getResourceColor(@StringRes resource: Int) : Int {
|
fun Resources.Theme.getResourceColor(@StringRes resource: Int): Int {
|
||||||
val typedArray = this.obtainStyledAttributes(intArrayOf(resource))
|
val typedArray = obtainStyledAttributes(intArrayOf(resource))
|
||||||
val attrValue = typedArray.getColor(0, 0)
|
val attrValue = typedArray.getColor(0, 0)
|
||||||
typedArray.recycle()
|
typedArray.recycle()
|
||||||
return attrValue
|
return attrValue
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Resources.Theme.getResourceDrawable(@StringRes resource: Int) : Drawable {
|
fun Resources.Theme.getResourceDrawable(@StringRes resource: Int): Drawable {
|
||||||
val typedArray = this.obtainStyledAttributes(intArrayOf(resource))
|
val typedArray = obtainStyledAttributes(intArrayOf(resource))
|
||||||
val attrValue = typedArray.getDrawable(0)
|
val attrValue = typedArray.getDrawable(0)
|
||||||
typedArray.recycle()
|
typedArray.recycle()
|
||||||
return attrValue
|
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
|
||||||
|
}
|
@ -37,8 +37,8 @@ class AutofitRecyclerView @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
|
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
|
||||||
super.onMeasure(widthSpec, heightSpec)
|
super.onMeasure(widthSpec, heightSpec)
|
||||||
if (spanCount == 0 && columnWidth > 0) {
|
if (spanCount == 0 && columnWidth > 0) {
|
||||||
val spanCount = Math.max(1, measuredWidth / columnWidth)
|
val count = Math.max(1, measuredWidth / columnWidth)
|
||||||
manager.spanCount = spanCount
|
spanCount = count
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,11 +1,13 @@
|
|||||||
package eu.kanade.tachiyomi.widget
|
package eu.kanade.tachiyomi.widget
|
||||||
|
|
||||||
import android.widget.SeekBar
|
import android.widget.SeekBar
|
||||||
|
|
||||||
open class SimpleSeekBarListener : SeekBar.OnSeekBarChangeListener {
|
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) {
|
||||||
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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)
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
BIN
app/src/main/res/drawable/filter_mock.png
Normal file
BIN
app/src/main/res/drawable/filter_mock.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 334 KiB |
@ -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>
|
9
app/src/main/res/drawable/ic_brightness_4_white_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_brightness_4_white_24dp.xml
Normal 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>
|
9
app/src/main/res/drawable/ic_brightness_5_black_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_brightness_5_black_24dp.xml
Normal 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>
|
9
app/src/main/res/drawable/ic_home_white_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_home_white_24dp.xml
Normal 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>
|
@ -1,9 +1,9 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:viewportWidth="24.0"
|
android:viewportHeight="24.0"
|
||||||
android:viewportHeight="24.0">
|
android:viewportWidth="24.0">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#ffffff"
|
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"/>
|
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>
|
</vector>
|
||||||
|
9
app/src/main/res/drawable/ic_watch_later_black_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_watch_later_black_24dp.xml
Normal 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>
|
BIN
app/src/main/res/drawable/mask_star.png
Normal file
BIN
app/src/main/res/drawable/mask_star.png
Normal file
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
Loading…
x
Reference in New Issue
Block a user