Compare commits
35 Commits
Author | SHA1 | Date | |
---|---|---|---|
91cb892c74 | |||
a26f908370 | |||
4d14f56fa8 | |||
d9a2255be9 | |||
5e3d71c6c5 | |||
619d94bf36 | |||
6069659e0f | |||
f6a79bde6f | |||
bb9e230b35 | |||
bc9417e16b | |||
a4313d388d | |||
4ebb3a894d | |||
0642889b64 | |||
3094d084d6 | |||
f9fec74ffd | |||
8ef3ab0d49 | |||
e9a6f8ef46 | |||
68724752f8 | |||
de8fa09366 | |||
e619870eec | |||
4be5f0dab3 | |||
abe1929b49 | |||
68c4116327 | |||
3be9881997 | |||
2e44f29882 | |||
a5520c1936 | |||
112cdd54e3 | |||
b512c67b5d | |||
d8fa7bc9d2 | |||
41397ab41d | |||
c437f1473c | |||
6020cd011d | |||
582bb3e2ca | |||
5c67161dce | |||
c00eaae62b |
@ -38,12 +38,13 @@ android {
|
|||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 25
|
targetSdkVersion 25
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
versionCode 20
|
versionCode 22
|
||||||
versionName "0.5.0"
|
versionName "0.5.2"
|
||||||
|
|
||||||
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
||||||
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
||||||
buildConfigField "String", "BUILD_TIME", "\"${getBuildTime()}\""
|
buildConfigField "String", "BUILD_TIME", "\"${getBuildTime()}\""
|
||||||
|
buildConfigField "boolean", "INCLUDE_UPDATER", "false"
|
||||||
|
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|
||||||
@ -70,9 +71,11 @@ android {
|
|||||||
standard {
|
standard {
|
||||||
buildConfigField "boolean", "INCLUDE_UPDATER", "true"
|
buildConfigField "boolean", "INCLUDE_UPDATER", "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
fdroid {
|
fdroid {
|
||||||
buildConfigField "boolean", "INCLUDE_UPDATER", "false"
|
}
|
||||||
|
dev {
|
||||||
|
minSdkVersion 21
|
||||||
|
resConfigs "en", "xxhdpi"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,11 +101,11 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
// Modified dependencies
|
// Modified dependencies
|
||||||
compile 'com.github.inorichi:subsampling-scale-image-view:4255750'
|
compile 'com.github.inorichi:subsampling-scale-image-view:01e5385'
|
||||||
compile 'com.github.inorichi:junrar-android:634c1f5'
|
compile 'com.github.inorichi:junrar-android:634c1f5'
|
||||||
|
|
||||||
// Android support library
|
// Android support library
|
||||||
final support_library_version = '25.2.0'
|
final support_library_version = '25.3.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"
|
||||||
@ -111,13 +114,13 @@ 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.constraint:constraint-layout:1.0.0'
|
compile 'com.android.support.constraint:constraint-layout:1.0.2'
|
||||||
|
|
||||||
compile 'com.android.support:multidex:1.0.1'
|
compile 'com.android.support:multidex:1.0.1'
|
||||||
|
|
||||||
// ReactiveX
|
// ReactiveX
|
||||||
compile 'io.reactivex:rxandroid:1.2.1'
|
compile 'io.reactivex:rxandroid:1.2.1'
|
||||||
compile 'io.reactivex:rxjava:1.2.6'
|
compile 'io.reactivex:rxjava:1.2.9'
|
||||||
compile 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
compile 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
||||||
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
|
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
|
||||||
compile 'com.github.pwittchen:reactivenetwork:0.7.0'
|
compile 'com.github.pwittchen:reactivenetwork:0.7.0'
|
||||||
@ -150,7 +153,7 @@ dependencies {
|
|||||||
compile 'org.jsoup:jsoup:1.10.2'
|
compile 'org.jsoup:jsoup:1.10.2'
|
||||||
|
|
||||||
// Job scheduling
|
// Job scheduling
|
||||||
compile 'com.evernote:android-job:1.1.6'
|
compile 'com.evernote:android-job:1.1.8'
|
||||||
compile 'com.google.android.gms:play-services-gcm:10.2.0'
|
compile 'com.google.android.gms:play-services-gcm:10.2.0'
|
||||||
|
|
||||||
// Changelog
|
// Changelog
|
||||||
@ -172,7 +175,7 @@ dependencies {
|
|||||||
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
|
// Transformations
|
||||||
compile 'jp.wasabeef:glide-transformations:2.0.1'
|
compile 'jp.wasabeef:glide-transformations:2.0.2'
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
compile 'com.jakewharton.timber:timber:4.5.1'
|
compile 'com.jakewharton.timber:timber:4.5.1'
|
||||||
@ -190,7 +193,7 @@ dependencies {
|
|||||||
compile 'com.github.inorichi:FlexibleAdapter:93985fe' // v4.2.0 to be removed
|
compile 'com.github.inorichi:FlexibleAdapter:93985fe' // v4.2.0 to be removed
|
||||||
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.9.3.0'
|
compile 'com.afollestad.material-dialogs:core:0.9.4.2'
|
||||||
compile 'net.xpece.android:support-preference:1.2.5'
|
compile 'net.xpece.android:support-preference:1.2.5'
|
||||||
compile 'me.zhanghai.android.systemuihelper:library:1.0.0'
|
compile 'me.zhanghai.android.systemuihelper:library:1.0.0'
|
||||||
compile 'de.hdodenhof:circleimageview:2.1.0'
|
compile 'de.hdodenhof:circleimageview:2.1.0'
|
||||||
@ -209,7 +212,7 @@ dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.0.6'
|
ext.kotlin_version = '1.1.1'
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
@ -221,3 +224,50 @@ buildscript {
|
|||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Workaround to force a support lib version
|
||||||
|
configurations.all {
|
||||||
|
resolutionStrategy.eachDependency { details ->
|
||||||
|
def requested = details.requested
|
||||||
|
if (requested.group == 'com.android.support') {
|
||||||
|
if (!requested.name.startsWith("multidex")) {
|
||||||
|
details.useVersion '25.3.1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add support for placeholders in resource files
|
||||||
|
//https://code.google.com/p/android/issues/detail?id=69224
|
||||||
|
def replacePlaceholdersInFile(basePath, fileName, placeholders) {
|
||||||
|
def file = new File(basePath, fileName);
|
||||||
|
|
||||||
|
if (!file.exists()) {
|
||||||
|
logger.quiet("Unable to replace placeholders in " + file.toString() + ". File cannot be found.")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("Replacing placeholders in " + file.toString())
|
||||||
|
logger.debug("Placeholders: " + placeholders.toString())
|
||||||
|
|
||||||
|
def content = file.getText('UTF-8')
|
||||||
|
|
||||||
|
placeholders.each { entry ->
|
||||||
|
content = content.replaceAll("\\\$\\{${entry.key}\\}", entry.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
file.write(content, 'UTF-8')
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEvaluate {
|
||||||
|
android.applicationVariants.all { variant ->
|
||||||
|
variant.outputs.each { output ->
|
||||||
|
output.processResources.doFirst {
|
||||||
|
// prepare placeholder map from manifestPlaceholders including applicationId placeholder
|
||||||
|
def placeholders = variant.mergedFlavor.manifestPlaceholders + [applicationId: variant.applicationId]
|
||||||
|
|
||||||
|
replacePlaceholdersInFile(resDir, 'xml-v25/shortcuts.xml', placeholders)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -19,6 +19,7 @@
|
|||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:theme="@style/Theme.Tachiyomi">
|
android:theme="@style/Theme.Tachiyomi">
|
||||||
@ -28,6 +29,8 @@
|
|||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<meta-data android:name="android.app.shortcuts"
|
||||||
|
android:resource="@xml/shortcuts"/>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.manga.MangaActivity"
|
android:name=".ui.manga.MangaActivity"
|
||||||
@ -45,7 +48,7 @@
|
|||||||
android:label="@string/label_categories"
|
android:label="@string/label_categories"
|
||||||
android:parentActivityName=".ui.main.MainActivity" />
|
android:parentActivityName=".ui.main.MainActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.SettingsDownloadsFragment$CustomLayoutPickerActivity"
|
android:name=".widget.CustomLayoutPickerActivity"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@style/FilePickerTheme" />
|
android:theme="@style/FilePickerTheme" />
|
||||||
<activity
|
<activity
|
||||||
@ -102,6 +105,14 @@
|
|||||||
android:name=".data.updater.UpdateDownloaderService"
|
android:name=".data.updater.UpdateDownloaderService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".data.backup.BackupCreateService"
|
||||||
|
android:exported="false"/>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".data.backup.BackupRestoreService"
|
||||||
|
android:exported="false"/>
|
||||||
|
|
||||||
<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" />
|
||||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 22 KiB |
@ -5,6 +5,7 @@ import android.content.Context
|
|||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.support.multidex.MultiDex
|
import android.support.multidex.MultiDex
|
||||||
import com.evernote.android.job.JobManager
|
import com.evernote.android.job.JobManager
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob
|
import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob
|
||||||
import eu.kanade.tachiyomi.util.LocaleHelper
|
import eu.kanade.tachiyomi.util.LocaleHelper
|
||||||
@ -58,6 +59,7 @@ open class App : Application() {
|
|||||||
when (tag) {
|
when (tag) {
|
||||||
LibraryUpdateJob.TAG -> LibraryUpdateJob()
|
LibraryUpdateJob.TAG -> LibraryUpdateJob()
|
||||||
UpdateCheckerJob.TAG -> UpdateCheckerJob()
|
UpdateCheckerJob.TAG -> UpdateCheckerJob()
|
||||||
|
BackupCreatorJob.TAG -> BackupCreatorJob()
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
package eu.kanade.tachiyomi
|
package eu.kanade.tachiyomi
|
||||||
|
|
||||||
object Constants {
|
object Constants {
|
||||||
const val NOTIFICATION_LIBRARY_ID = 1
|
const val NOTIFICATION_LIBRARY_PROGRESS_ID = 1
|
||||||
const val NOTIFICATION_UPDATER_ID = 2
|
const val NOTIFICATION_LIBRARY_RESULT_ID = 2
|
||||||
const val NOTIFICATION_DOWNLOAD_CHAPTER_ID = 3
|
const val NOTIFICATION_UPDATER_ID = 3
|
||||||
const val NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID = 4
|
const val NOTIFICATION_DOWNLOAD_CHAPTER_ID = 4
|
||||||
const val NOTIFICATION_DOWNLOAD_IMAGE_ID = 5
|
const val NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID = 5
|
||||||
|
const val NOTIFICATION_DOWNLOAD_IMAGE_ID = 6
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,171 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
|
import android.app.IntentService
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import com.github.salomonbrys.kotson.set
|
||||||
|
import com.google.gson.JsonArray
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.CATEGORIES
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGAS
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.VERSION
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.ui.setting.SettingsBackupFragment
|
||||||
|
import eu.kanade.tachiyomi.util.AndroidComponentUtil
|
||||||
|
import eu.kanade.tachiyomi.util.sendLocalBroadcast
|
||||||
|
import timber.log.Timber
|
||||||
|
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [IntentService] used to backup [Manga] information to [JsonArray]
|
||||||
|
*/
|
||||||
|
class BackupCreateService : IntentService(NAME) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// Name of class
|
||||||
|
private const val NAME = "BackupCreateService"
|
||||||
|
|
||||||
|
// Uri as string
|
||||||
|
private const val EXTRA_URI = "$ID.$NAME.EXTRA_URI"
|
||||||
|
// Backup called from job
|
||||||
|
private const val EXTRA_IS_JOB = "$ID.$NAME.EXTRA_IS_JOB"
|
||||||
|
// Options for backup
|
||||||
|
private const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS"
|
||||||
|
|
||||||
|
// Filter options
|
||||||
|
internal const val BACKUP_CATEGORY = 0x1
|
||||||
|
internal const val BACKUP_CATEGORY_MASK = 0x1
|
||||||
|
internal const val BACKUP_CHAPTER = 0x2
|
||||||
|
internal const val BACKUP_CHAPTER_MASK = 0x2
|
||||||
|
internal const val BACKUP_HISTORY = 0x4
|
||||||
|
internal const val BACKUP_HISTORY_MASK = 0x4
|
||||||
|
internal const val BACKUP_TRACK = 0x8
|
||||||
|
internal const val BACKUP_TRACK_MASK = 0x8
|
||||||
|
internal const val BACKUP_ALL = 0xF
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a backup from library
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @param path path of Uri
|
||||||
|
* @param flags determines what to backup
|
||||||
|
* @param isJob backup called from job
|
||||||
|
*/
|
||||||
|
fun makeBackup(context: Context, path: String, flags: Int, isJob: Boolean = false) {
|
||||||
|
val intent = Intent(context, BackupCreateService::class.java).apply {
|
||||||
|
putExtra(EXTRA_URI, path)
|
||||||
|
putExtra(EXTRA_IS_JOB, isJob)
|
||||||
|
putExtra(EXTRA_FLAGS, flags)
|
||||||
|
}
|
||||||
|
context.startService(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isRunning(context: Context): Boolean {
|
||||||
|
return AndroidComponentUtil.isServiceRunning(context, BackupCreateService::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val backupManager by lazy { BackupManager(this) }
|
||||||
|
|
||||||
|
override fun onHandleIntent(intent: Intent?) {
|
||||||
|
if (intent == null) return
|
||||||
|
|
||||||
|
// Get values
|
||||||
|
val uri = intent.getStringExtra(EXTRA_URI)
|
||||||
|
val isJob = intent.getBooleanExtra(EXTRA_IS_JOB, false)
|
||||||
|
val flags = intent.getIntExtra(EXTRA_FLAGS, 0)
|
||||||
|
// Create backup
|
||||||
|
createBackupFromApp(Uri.parse(uri), flags, isJob)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create backup Json file from database
|
||||||
|
*
|
||||||
|
* @param uri path of Uri
|
||||||
|
* @param isJob backup called from job
|
||||||
|
*/
|
||||||
|
fun createBackupFromApp(uri: Uri, flags: Int, isJob: Boolean) {
|
||||||
|
// Create root object
|
||||||
|
val root = JsonObject()
|
||||||
|
|
||||||
|
// Create information object
|
||||||
|
val information = JsonObject()
|
||||||
|
|
||||||
|
// Create manga array
|
||||||
|
val mangaEntries = JsonArray()
|
||||||
|
|
||||||
|
// Create category array
|
||||||
|
val categoryEntries = JsonArray()
|
||||||
|
|
||||||
|
// Add value's to root
|
||||||
|
root[VERSION] = Backup.CURRENT_VERSION
|
||||||
|
root[MANGAS] = mangaEntries
|
||||||
|
root[CATEGORIES] = categoryEntries
|
||||||
|
|
||||||
|
backupManager.databaseHelper.inTransaction {
|
||||||
|
// Get manga from database
|
||||||
|
val mangas = backupManager.getFavoriteManga()
|
||||||
|
|
||||||
|
// Backup library manga and its dependencies
|
||||||
|
mangas.forEach { manga ->
|
||||||
|
mangaEntries.add(backupManager.backupMangaObject(manga, flags))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup categories
|
||||||
|
if ((flags and BACKUP_CATEGORY_MASK) == BACKUP_CATEGORY) {
|
||||||
|
backupManager.backupCategories(categoryEntries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// When BackupCreatorJob
|
||||||
|
if (isJob) {
|
||||||
|
// Get dir of file
|
||||||
|
val dir = UniFile.fromUri(this, uri)
|
||||||
|
|
||||||
|
// Delete older backups
|
||||||
|
val numberOfBackups = backupManager.numberOfBackups()
|
||||||
|
val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.json""")
|
||||||
|
dir.listFiles { _, filename -> backupRegex.matches(filename) }
|
||||||
|
.orEmpty()
|
||||||
|
.sortedByDescending { it.name }
|
||||||
|
.drop(numberOfBackups - 1)
|
||||||
|
.forEach { it.delete() }
|
||||||
|
|
||||||
|
// Create new file to place backup
|
||||||
|
val newFile = dir.createFile(Backup.getDefaultFilename())
|
||||||
|
?: throw Exception("Couldn't create backup file")
|
||||||
|
|
||||||
|
newFile.openOutputStream().bufferedWriter().use {
|
||||||
|
backupManager.parser.toJson(root, it)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val file = UniFile.fromUri(this, uri)
|
||||||
|
?: throw Exception("Couldn't create backup file")
|
||||||
|
file.openOutputStream().bufferedWriter().use {
|
||||||
|
backupManager.parser.toJson(root, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show completed dialog
|
||||||
|
val intent = Intent(SettingsBackupFragment.INTENT_FILTER).apply {
|
||||||
|
putExtra(SettingsBackupFragment.ACTION, SettingsBackupFragment.ACTION_BACKUP_COMPLETED_DIALOG)
|
||||||
|
putExtra(SettingsBackupFragment.EXTRA_URI, file.uri.toString())
|
||||||
|
}
|
||||||
|
sendLocalBroadcast(intent)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
if (!isJob) {
|
||||||
|
// Show error dialog
|
||||||
|
val intent = Intent(SettingsBackupFragment.INTENT_FILTER).apply {
|
||||||
|
putExtra(SettingsBackupFragment.ACTION, SettingsBackupFragment.ACTION_ERROR_BACKUP_DIALOG)
|
||||||
|
putExtra(SettingsBackupFragment.EXTRA_ERROR_MESSAGE, e.message)
|
||||||
|
}
|
||||||
|
sendLocalBroadcast(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
|
import com.evernote.android.job.Job
|
||||||
|
import com.evernote.android.job.JobManager
|
||||||
|
import com.evernote.android.job.JobRequest
|
||||||
|
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 BackupCreatorJob : Job() {
|
||||||
|
|
||||||
|
override fun onRunJob(params: Params): Result {
|
||||||
|
val preferences = Injekt.get<PreferencesHelper>()
|
||||||
|
val path = preferences.backupsDirectory().getOrDefault()
|
||||||
|
val flags = BackupCreateService.BACKUP_ALL
|
||||||
|
BackupCreateService.makeBackup(context,path,flags,true)
|
||||||
|
return Result.SUCCESS
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "BackupCreator"
|
||||||
|
|
||||||
|
fun setupTask(prefInterval: Int? = null) {
|
||||||
|
val preferences = Injekt.get<PreferencesHelper>()
|
||||||
|
val interval = prefInterval ?: preferences.backupInterval().getOrDefault()
|
||||||
|
if (interval > 0) {
|
||||||
|
JobRequest.Builder(TAG)
|
||||||
|
.setPeriodic(interval * 60 * 60 * 1000L, 10 * 60 * 1000)
|
||||||
|
.setPersisted(true)
|
||||||
|
.setUpdateCurrent(true)
|
||||||
|
.build()
|
||||||
|
.schedule()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelTask() {
|
||||||
|
JobManager.instance().cancelAllForTag(TAG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,203 +1,213 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.fromJson
|
import android.content.Context
|
||||||
|
import com.github.salomonbrys.kotson.*
|
||||||
import com.google.gson.*
|
import com.google.gson.*
|
||||||
import com.google.gson.stream.JsonReader
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY
|
||||||
import eu.kanade.tachiyomi.data.backup.serializer.BooleanSerializer
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK
|
||||||
import eu.kanade.tachiyomi.data.backup.serializer.IdExclusion
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER
|
||||||
import eu.kanade.tachiyomi.data.backup.serializer.IntegerSerializer
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER_MASK
|
||||||
import eu.kanade.tachiyomi.data.backup.serializer.LongSerializer
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK_MASK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.CATEGORIES
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.CHAPTERS
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.CURRENT_VERSION
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
||||||
|
import eu.kanade.tachiyomi.data.backup.serializer.*
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.*
|
import eu.kanade.tachiyomi.data.database.models.*
|
||||||
import java.io.*
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.util.syncChaptersWithSource
|
||||||
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/**
|
class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||||
* This class provides the necessary methods to create and restore backups for the data of the
|
|
||||||
* application. The backup follows a JSON structure, with the following scheme:
|
|
||||||
*
|
|
||||||
* {
|
|
||||||
* "mangas": [
|
|
||||||
* {
|
|
||||||
* "manga": {"id": 1, ...},
|
|
||||||
* "chapters": [{"id": 1, ...}, {...}],
|
|
||||||
* "sync": [{"id": 1, ...}, {...}],
|
|
||||||
* "categories": ["cat1", "cat2", ...]
|
|
||||||
* },
|
|
||||||
* { ... }
|
|
||||||
* ],
|
|
||||||
* "categories": [
|
|
||||||
* {"id": 1, ...},
|
|
||||||
* {"id": 2, ...}
|
|
||||||
* ]
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* @param db the database helper.
|
|
||||||
*/
|
|
||||||
class BackupManager(private val db: DatabaseHelper) {
|
|
||||||
|
|
||||||
private val MANGA = "manga"
|
|
||||||
private val MANGAS = "mangas"
|
|
||||||
private val CHAPTERS = "chapters"
|
|
||||||
private val TRACK = "sync"
|
|
||||||
private val CATEGORIES = "categories"
|
|
||||||
|
|
||||||
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
|
|
||||||
private val gson = GsonBuilder()
|
|
||||||
.registerTypeAdapter(java.lang.Integer::class.java, IntegerSerializer())
|
|
||||||
.registerTypeAdapter(java.lang.Boolean::class.java, BooleanSerializer())
|
|
||||||
.registerTypeAdapter(java.lang.Long::class.java, LongSerializer())
|
|
||||||
.setExclusionStrategies(IdExclusion())
|
|
||||||
.create()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Backups the data of the application to a file.
|
* Database.
|
||||||
*
|
|
||||||
* @param file the file where the backup will be saved.
|
|
||||||
* @throws IOException if there's any IO error.
|
|
||||||
*/
|
*/
|
||||||
@Throws(IOException::class)
|
internal val databaseHelper: DatabaseHelper by injectLazy()
|
||||||
fun backupToFile(file: File) {
|
|
||||||
val root = backupToJson()
|
|
||||||
|
|
||||||
FileWriter(file).use {
|
/**
|
||||||
gson.toJson(root, it)
|
* Source manager.
|
||||||
|
*/
|
||||||
|
internal val sourceManager: SourceManager by injectLazy()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Version of parser
|
||||||
|
*/
|
||||||
|
var version: Int = version
|
||||||
|
private set
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Json Parser
|
||||||
|
*/
|
||||||
|
var parser: Gson = initParser()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preferences
|
||||||
|
*/
|
||||||
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set version of parser
|
||||||
|
*
|
||||||
|
* @param version version of parser
|
||||||
|
*/
|
||||||
|
internal fun setVersion(version: Int) {
|
||||||
|
this.version = version
|
||||||
|
parser = initParser()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initParser(): Gson {
|
||||||
|
return when (version) {
|
||||||
|
1 -> GsonBuilder().create()
|
||||||
|
2 -> GsonBuilder()
|
||||||
|
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
|
||||||
|
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
|
||||||
|
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
|
||||||
|
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
|
||||||
|
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
|
||||||
|
.create()
|
||||||
|
else -> throw Exception("Json version unknown")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a JSON object containing the backup of the app's data.
|
* Backup the categories of library
|
||||||
*
|
*
|
||||||
* @return the backup as a JSON object.
|
* @param root root of categories json
|
||||||
*/
|
*/
|
||||||
fun backupToJson(): JsonObject {
|
internal fun backupCategories(root: JsonArray) {
|
||||||
val root = JsonObject()
|
val categories = databaseHelper.getCategories().executeAsBlocking()
|
||||||
|
categories.forEach { root.add(parser.toJsonTree(it)) }
|
||||||
// Backup library mangas and its dependencies
|
|
||||||
val mangaEntries = JsonArray()
|
|
||||||
root.add(MANGAS, mangaEntries)
|
|
||||||
for (manga in db.getFavoriteMangas().executeAsBlocking()) {
|
|
||||||
mangaEntries.add(backupManga(manga))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backup categories
|
|
||||||
val categoryEntries = JsonArray()
|
|
||||||
root.add(CATEGORIES, categoryEntries)
|
|
||||||
for (category in db.getCategories().executeAsBlocking()) {
|
|
||||||
categoryEntries.add(backupCategory(category))
|
|
||||||
}
|
|
||||||
|
|
||||||
return root
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Backups a manga and its related data (chapters, categories this manga is in, sync...).
|
* Convert a manga to Json
|
||||||
*
|
*
|
||||||
* @param manga the manga to backup.
|
* @param manga manga that gets converted
|
||||||
* @return a JSON object containing all the data of the manga.
|
* @return [JsonElement] containing manga information
|
||||||
*/
|
*/
|
||||||
private fun backupManga(manga: Manga): JsonObject {
|
internal fun backupMangaObject(manga: Manga, options: Int): JsonElement {
|
||||||
// Entry for this manga
|
// Entry for this manga
|
||||||
val entry = JsonObject()
|
val entry = JsonObject()
|
||||||
|
|
||||||
// Backup manga fields
|
// Backup manga fields
|
||||||
entry.add(MANGA, gson.toJsonTree(manga))
|
entry[MANGA] = parser.toJsonTree(manga)
|
||||||
|
|
||||||
// Backup all the chapters
|
// Check if user wants chapter information in backup
|
||||||
val chapters = db.getChapters(manga).executeAsBlocking()
|
if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) {
|
||||||
if (!chapters.isEmpty()) {
|
// Backup all the chapters
|
||||||
entry.add(CHAPTERS, gson.toJsonTree(chapters))
|
val chapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
||||||
}
|
if (!chapters.isEmpty()) {
|
||||||
|
val chaptersJson = parser.toJsonTree(chapters)
|
||||||
// Backup tracks
|
if (chaptersJson.asJsonArray.size() > 0) {
|
||||||
val tracks = db.getTracks(manga).executeAsBlocking()
|
entry[CHAPTERS] = chaptersJson
|
||||||
if (!tracks.isEmpty()) {
|
}
|
||||||
entry.add(TRACK, gson.toJsonTree(tracks))
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backup categories for this manga
|
// Check if user wants category information in backup
|
||||||
val categoriesForManga = db.getCategoriesForManga(manga).executeAsBlocking()
|
if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
|
||||||
if (!categoriesForManga.isEmpty()) {
|
// Backup categories for this manga
|
||||||
val categoriesNames = ArrayList<String>()
|
val categoriesForManga = databaseHelper.getCategoriesForManga(manga).executeAsBlocking()
|
||||||
for (category in categoriesForManga) {
|
if (!categoriesForManga.isEmpty()) {
|
||||||
categoriesNames.add(category.name)
|
val categoriesNames = categoriesForManga.map { it.name }
|
||||||
|
entry[CATEGORIES] = parser.toJsonTree(categoriesNames)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user wants track information in backup
|
||||||
|
if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) {
|
||||||
|
val tracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
||||||
|
if (!tracks.isEmpty()) {
|
||||||
|
entry[TRACK] = parser.toJsonTree(tracks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user wants history information in backup
|
||||||
|
if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) {
|
||||||
|
val historyForManga = databaseHelper.getHistoryByMangaId(manga.id!!).executeAsBlocking()
|
||||||
|
if (!historyForManga.isEmpty()) {
|
||||||
|
val historyData = historyForManga.mapNotNull { history ->
|
||||||
|
val url = databaseHelper.getChapter(history.chapter_id).executeAsBlocking()?.url
|
||||||
|
url?.let { DHistory(url, history.last_read) }
|
||||||
|
}
|
||||||
|
val historyJson = parser.toJsonTree(historyData)
|
||||||
|
if (historyJson.asJsonArray.size() > 0) {
|
||||||
|
entry[HISTORY] = historyJson
|
||||||
|
}
|
||||||
}
|
}
|
||||||
entry.add(CATEGORIES, gson.toJsonTree(categoriesNames))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return entry
|
return entry
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
fun restoreMangaNoFetch(manga: Manga, dbManga: Manga) {
|
||||||
* Backups a category.
|
manga.id = dbManga.id
|
||||||
*
|
manga.copyFrom(dbManga)
|
||||||
* @param category the category to backup.
|
manga.favorite = true
|
||||||
* @return a JSON object containing the data of the category.
|
insertManga(manga)
|
||||||
*/
|
|
||||||
private fun backupCategory(category: Category): JsonElement {
|
|
||||||
return gson.toJsonTree(category)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restores a backup from a file.
|
* [Observable] that fetches manga information
|
||||||
*
|
*
|
||||||
* @param file the file containing the backup.
|
* @param source source of manga
|
||||||
* @throws IOException if there's any IO error.
|
* @param manga manga that needs updating
|
||||||
|
* @return [Observable] that contains manga
|
||||||
*/
|
*/
|
||||||
@Throws(IOException::class)
|
fun restoreMangaFetchObservable(source: Source, manga: Manga): Observable<Manga> {
|
||||||
fun restoreFromFile(file: File) {
|
return source.fetchMangaDetails(manga)
|
||||||
JsonReader(FileReader(file)).use {
|
.map { networkManga ->
|
||||||
val root = JsonParser().parse(it).asJsonObject
|
manga.copyFrom(networkManga)
|
||||||
restoreFromJson(root)
|
manga.favorite = true
|
||||||
}
|
manga.initialized = true
|
||||||
|
manga.id = insertManga(manga)
|
||||||
|
manga
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restores a backup from an input stream.
|
* [Observable] that fetches chapter information
|
||||||
*
|
*
|
||||||
* @param stream the stream containing the backup.
|
* @param source source of manga
|
||||||
* @throws IOException if there's any IO error.
|
* @param manga manga that needs updating
|
||||||
|
* @return [Observable] that contains manga
|
||||||
*/
|
*/
|
||||||
@Throws(IOException::class)
|
fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
||||||
fun restoreFromStream(stream: InputStream) {
|
return source.fetchChapterList(manga)
|
||||||
JsonReader(InputStreamReader(stream)).use {
|
.map { syncChaptersWithSource(databaseHelper, it, manga, source) }
|
||||||
val root = JsonParser().parse(it).asJsonObject
|
.doOnNext {
|
||||||
restoreFromJson(root)
|
if (it.first.isNotEmpty()) {
|
||||||
}
|
chapters.forEach { it.manga_id = manga.id }
|
||||||
|
insertChapters(chapters)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restores a backup from a JSON object. Everything executes in a single transaction so that
|
* Restore the categories from Json
|
||||||
* nothing is modified if there's an error.
|
|
||||||
*
|
*
|
||||||
* @param root the root of the JSON.
|
* @param jsonCategories array containing categories
|
||||||
*/
|
*/
|
||||||
fun restoreFromJson(root: JsonObject) {
|
internal fun restoreCategories(jsonCategories: JsonArray) {
|
||||||
db.inTransaction {
|
|
||||||
// Restore categories
|
|
||||||
root.get(CATEGORIES)?.let {
|
|
||||||
restoreCategories(it.asJsonArray)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore mangas
|
|
||||||
root.get(MANGAS)?.let {
|
|
||||||
restoreMangas(it.asJsonArray)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restores the categories.
|
|
||||||
*
|
|
||||||
* @param jsonCategories the categories of the json.
|
|
||||||
*/
|
|
||||||
private fun restoreCategories(jsonCategories: JsonArray) {
|
|
||||||
// Get categories from file and from db
|
// Get categories from file and from db
|
||||||
val dbCategories = db.getCategories().executeAsBlocking()
|
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
||||||
val backupCategories = gson.fromJson<List<CategoryImpl>>(jsonCategories)
|
val backupCategories = parser.fromJson<List<CategoryImpl>>(jsonCategories)
|
||||||
|
|
||||||
// Iterate over them
|
// Iterate over them
|
||||||
for (category in backupCategories) {
|
backupCategories.forEach { category ->
|
||||||
// Used to know if the category is already in the db
|
// Used to know if the category is already in the db
|
||||||
var found = false
|
var found = false
|
||||||
for (dbCategory in dbCategories) {
|
for (dbCategory in dbCategories) {
|
||||||
@ -214,102 +224,20 @@ class BackupManager(private val db: DatabaseHelper) {
|
|||||||
if (!found) {
|
if (!found) {
|
||||||
// Let the db assign the id
|
// Let the db assign the id
|
||||||
category.id = null
|
category.id = null
|
||||||
val result = db.insertCategory(category).executeAsBlocking()
|
val result = databaseHelper.insertCategory(category).executeAsBlocking()
|
||||||
category.id = result.insertedId()?.toInt()
|
category.id = result.insertedId()?.toInt()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Restores all the mangas and its related data.
|
|
||||||
*
|
|
||||||
* @param jsonMangas the mangas and its related data (chapters, sync, categories) from the json.
|
|
||||||
*/
|
|
||||||
private fun restoreMangas(jsonMangas: JsonArray) {
|
|
||||||
for (backupManga in jsonMangas) {
|
|
||||||
// Map every entry to objects
|
|
||||||
val element = backupManga.asJsonObject
|
|
||||||
val manga = gson.fromJson(element.get(MANGA), MangaImpl::class.java)
|
|
||||||
val chapters = gson.fromJson<List<ChapterImpl>>(element.get(CHAPTERS) ?: JsonArray())
|
|
||||||
val tracks = gson.fromJson<List<TrackImpl>>(element.get(TRACK) ?: JsonArray())
|
|
||||||
val categories = gson.fromJson<List<String>>(element.get(CATEGORIES) ?: JsonArray())
|
|
||||||
|
|
||||||
// Restore everything related to this manga
|
|
||||||
restoreManga(manga)
|
|
||||||
restoreChaptersForManga(manga, chapters)
|
|
||||||
restoreSyncForManga(manga, tracks)
|
|
||||||
restoreCategoriesForManga(manga, categories)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restores a manga.
|
|
||||||
*
|
|
||||||
* @param manga the manga to restore.
|
|
||||||
*/
|
|
||||||
private fun restoreManga(manga: Manga) {
|
|
||||||
// Try to find existing manga in db
|
|
||||||
val dbManga = db.getManga(manga.url, manga.source).executeAsBlocking()
|
|
||||||
if (dbManga == null) {
|
|
||||||
// Let the db assign the id
|
|
||||||
manga.id = null
|
|
||||||
val result = db.insertManga(manga).executeAsBlocking()
|
|
||||||
manga.id = result.insertedId()
|
|
||||||
} else {
|
|
||||||
// If it exists already, we copy only the values related to the source from the db
|
|
||||||
// (they can be up to date). Local values (flags) are kept from the backup.
|
|
||||||
manga.id = dbManga.id
|
|
||||||
manga.copyFrom(dbManga)
|
|
||||||
manga.favorite = true
|
|
||||||
db.insertManga(manga).executeAsBlocking()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restores the chapters of a manga.
|
|
||||||
*
|
|
||||||
* @param manga the manga whose chapters have to be restored.
|
|
||||||
* @param chapters the chapters to restore.
|
|
||||||
*/
|
|
||||||
private fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>) {
|
|
||||||
// Fix foreign keys with the current manga id
|
|
||||||
for (chapter in chapters) {
|
|
||||||
chapter.manga_id = manga.id
|
|
||||||
}
|
|
||||||
|
|
||||||
val dbChapters = db.getChapters(manga).executeAsBlocking()
|
|
||||||
val chaptersToUpdate = ArrayList<Chapter>()
|
|
||||||
for (backupChapter in chapters) {
|
|
||||||
// Try to find existing chapter in db
|
|
||||||
val pos = dbChapters.indexOf(backupChapter)
|
|
||||||
if (pos != -1) {
|
|
||||||
// The chapter is already in the db, only update its fields
|
|
||||||
val dbChapter = dbChapters[pos]
|
|
||||||
// If one of them was read, the chapter will be marked as read
|
|
||||||
dbChapter.read = backupChapter.read || dbChapter.read
|
|
||||||
dbChapter.last_page_read = Math.max(backupChapter.last_page_read, dbChapter.last_page_read)
|
|
||||||
chaptersToUpdate.add(dbChapter)
|
|
||||||
} else {
|
|
||||||
// Insert new chapter. Let the db assign the id
|
|
||||||
backupChapter.id = null
|
|
||||||
chaptersToUpdate.add(backupChapter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update database
|
|
||||||
if (!chaptersToUpdate.isEmpty()) {
|
|
||||||
db.insertChapters(chaptersToUpdate).executeAsBlocking()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restores the categories a manga is in.
|
* Restores the categories a manga is in.
|
||||||
*
|
*
|
||||||
* @param manga the manga whose categories have to be restored.
|
* @param manga the manga whose categories have to be restored.
|
||||||
* @param categories the categories to restore.
|
* @param categories the categories to restore.
|
||||||
*/
|
*/
|
||||||
private fun restoreCategoriesForManga(manga: Manga, categories: List<String>) {
|
internal fun restoreCategoriesForManga(manga: Manga, categories: List<String>) {
|
||||||
val dbCategories = db.getCategories().executeAsBlocking()
|
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
||||||
val mangaCategoriesToUpdate = ArrayList<MangaCategory>()
|
val mangaCategoriesToUpdate = ArrayList<MangaCategory>()
|
||||||
for (backupCategoryStr in categories) {
|
for (backupCategoryStr in categories) {
|
||||||
for (dbCategory in dbCategories) {
|
for (dbCategory in dbCategories) {
|
||||||
@ -324,45 +252,151 @@ class BackupManager(private val db: DatabaseHelper) {
|
|||||||
if (!mangaCategoriesToUpdate.isEmpty()) {
|
if (!mangaCategoriesToUpdate.isEmpty()) {
|
||||||
val mangaAsList = ArrayList<Manga>()
|
val mangaAsList = ArrayList<Manga>()
|
||||||
mangaAsList.add(manga)
|
mangaAsList.add(manga)
|
||||||
db.deleteOldMangasCategories(mangaAsList).executeAsBlocking()
|
databaseHelper.deleteOldMangasCategories(mangaAsList).executeAsBlocking()
|
||||||
db.insertMangasCategories(mangaCategoriesToUpdate).executeAsBlocking()
|
databaseHelper.insertMangasCategories(mangaCategoriesToUpdate).executeAsBlocking()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore history from Json
|
||||||
|
*
|
||||||
|
* @param history list containing history to be restored
|
||||||
|
*/
|
||||||
|
internal fun restoreHistoryForManga(history: List<DHistory>) {
|
||||||
|
// List containing history to be updated
|
||||||
|
val historyToBeUpdated = ArrayList<History>()
|
||||||
|
for ((url, lastRead) in history) {
|
||||||
|
val dbHistory = databaseHelper.getHistoryByChapterUrl(url).executeAsBlocking()
|
||||||
|
// Check if history already in database and update
|
||||||
|
if (dbHistory != null) {
|
||||||
|
dbHistory.apply {
|
||||||
|
last_read = Math.max(lastRead, dbHistory.last_read)
|
||||||
|
}
|
||||||
|
historyToBeUpdated.add(dbHistory)
|
||||||
|
} else {
|
||||||
|
// If not in database create
|
||||||
|
databaseHelper.getChapter(url).executeAsBlocking()?.let {
|
||||||
|
val historyToAdd = History.create(it).apply {
|
||||||
|
last_read = lastRead
|
||||||
|
}
|
||||||
|
historyToBeUpdated.add(historyToAdd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
databaseHelper.updateHistoryLastRead(historyToBeUpdated).executeAsBlocking()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restores the sync of a manga.
|
* Restores the sync of a manga.
|
||||||
*
|
*
|
||||||
* @param manga the manga whose sync have to be restored.
|
* @param manga the manga whose sync have to be restored.
|
||||||
* @param tracks the track list to restore.
|
* @param tracks the track list to restore.
|
||||||
*/
|
*/
|
||||||
private fun restoreSyncForManga(manga: Manga, tracks: List<Track>) {
|
internal fun restoreTrackForManga(manga: Manga, tracks: List<Track>) {
|
||||||
// Fix foreign keys with the current manga id
|
// Fix foreign keys with the current manga id
|
||||||
for (track in tracks) {
|
tracks.map { it.manga_id = manga.id!! }
|
||||||
track.manga_id = manga.id!!
|
|
||||||
}
|
|
||||||
|
|
||||||
val dbTracks = db.getTracks(manga).executeAsBlocking()
|
// Get tracks from database
|
||||||
|
val dbTracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
||||||
val trackToUpdate = ArrayList<Track>()
|
val trackToUpdate = ArrayList<Track>()
|
||||||
for (backupTrack in tracks) {
|
|
||||||
// Try to find existing chapter in db
|
for (track in tracks) {
|
||||||
val pos = dbTracks.indexOf(backupTrack)
|
var isInDatabase = false
|
||||||
if (pos != -1) {
|
for (dbTrack in dbTracks) {
|
||||||
// The sync is already in the db, only update its fields
|
if (track.sync_id == dbTrack.sync_id) {
|
||||||
val dbSync = dbTracks[pos]
|
// The sync is already in the db, only update its fields
|
||||||
// Mark the max chapter as read and nothing else
|
if (track.remote_id != dbTrack.remote_id) {
|
||||||
dbSync.last_chapter_read = Math.max(backupTrack.last_chapter_read, dbSync.last_chapter_read)
|
dbTrack.remote_id = track.remote_id
|
||||||
trackToUpdate.add(dbSync)
|
}
|
||||||
} else {
|
dbTrack.last_chapter_read = Math.max(dbTrack.last_chapter_read, track.last_chapter_read)
|
||||||
|
isInDatabase = true
|
||||||
|
trackToUpdate.add(dbTrack)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isInDatabase) {
|
||||||
// Insert new sync. Let the db assign the id
|
// Insert new sync. Let the db assign the id
|
||||||
backupTrack.id = null
|
track.id = null
|
||||||
trackToUpdate.add(backupTrack)
|
trackToUpdate.add(track)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update database
|
// Update database
|
||||||
if (!trackToUpdate.isEmpty()) {
|
if (!trackToUpdate.isEmpty()) {
|
||||||
db.insertTracks(trackToUpdate).executeAsBlocking()
|
databaseHelper.insertTracks(trackToUpdate).executeAsBlocking()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore the chapters for manga if chapters already in database
|
||||||
|
*
|
||||||
|
* @param manga manga of chapters
|
||||||
|
* @param chapters list containing chapters that get restored
|
||||||
|
* @return boolean answering if chapter fetch is not needed
|
||||||
|
*/
|
||||||
|
internal fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>): Boolean {
|
||||||
|
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
||||||
|
|
||||||
|
// Return if fetch is needed
|
||||||
|
if (dbChapters.isEmpty() || dbChapters.size < chapters.size)
|
||||||
|
return false
|
||||||
|
|
||||||
|
for (chapter in chapters) {
|
||||||
|
val pos = dbChapters.indexOf(chapter)
|
||||||
|
if (pos != -1) {
|
||||||
|
val dbChapter = dbChapters[pos]
|
||||||
|
chapter.id = dbChapter.id
|
||||||
|
chapter.copyFrom(dbChapter)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Filter the chapters that couldn't be found.
|
||||||
|
chapters.filter { it.id != null }
|
||||||
|
chapters.map { it.manga_id = manga.id }
|
||||||
|
|
||||||
|
insertChapters(chapters)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns manga
|
||||||
|
*
|
||||||
|
* @return [Manga], null if not found
|
||||||
|
*/
|
||||||
|
internal fun getMangaFromDatabase(manga: Manga): Manga? {
|
||||||
|
return databaseHelper.getManga(manga.url, manga.source).executeAsBlocking()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns list containing manga from library
|
||||||
|
*
|
||||||
|
* @return [Manga] from library
|
||||||
|
*/
|
||||||
|
internal fun getFavoriteManga(): List<Manga> {
|
||||||
|
return databaseHelper.getFavoriteMangas().executeAsBlocking()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts manga and returns id
|
||||||
|
*
|
||||||
|
* @return id of [Manga], null if not found
|
||||||
|
*/
|
||||||
|
internal fun insertManga(manga: Manga): Long? {
|
||||||
|
return databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts list of chapters
|
||||||
|
*/
|
||||||
|
internal fun insertChapters(chapters: List<Chapter>) {
|
||||||
|
databaseHelper.updateChaptersBackup(chapters).executeAsBlocking()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return number of backups.
|
||||||
|
*
|
||||||
|
* @return number of backups selected by user
|
||||||
|
*/
|
||||||
|
fun numberOfBackups(): Int {
|
||||||
|
return preferences.numberOfBackups().getOrDefault()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,414 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.os.PowerManager
|
||||||
|
import com.github.salomonbrys.kotson.fromJson
|
||||||
|
import com.google.gson.JsonArray
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import com.google.gson.stream.JsonReader
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.CATEGORIES
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.CHAPTERS
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGAS
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.VERSION
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
||||||
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.*
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.ui.setting.SettingsBackupFragment
|
||||||
|
import eu.kanade.tachiyomi.util.AndroidComponentUtil
|
||||||
|
import eu.kanade.tachiyomi.util.chop
|
||||||
|
import eu.kanade.tachiyomi.util.sendLocalBroadcast
|
||||||
|
import rx.Observable
|
||||||
|
import rx.Subscription
|
||||||
|
import rx.schedulers.Schedulers
|
||||||
|
import timber.log.Timber
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.File
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.ExecutorService
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores backup from json file
|
||||||
|
*/
|
||||||
|
class BackupRestoreService : Service() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// Name of service
|
||||||
|
private const val NAME = "BackupRestoreService"
|
||||||
|
|
||||||
|
// Uri as string
|
||||||
|
private const val EXTRA_URI = "$ID.$NAME.EXTRA_URI"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the status of the service.
|
||||||
|
*
|
||||||
|
* @param context the application context.
|
||||||
|
* @return true if the service is running, false otherwise.
|
||||||
|
*/
|
||||||
|
fun isRunning(context: Context): Boolean {
|
||||||
|
return AndroidComponentUtil.isServiceRunning(context, BackupRestoreService::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a service to restore a backup from Json
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @param uri path of Uri
|
||||||
|
*/
|
||||||
|
fun start(context: Context, uri: String) {
|
||||||
|
if (!isRunning(context)) {
|
||||||
|
val intent = Intent(context, BackupRestoreService::class.java).apply {
|
||||||
|
putExtra(EXTRA_URI, uri)
|
||||||
|
}
|
||||||
|
context.startService(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the service.
|
||||||
|
*
|
||||||
|
* @param context the application context.
|
||||||
|
*/
|
||||||
|
fun stop(context: Context) {
|
||||||
|
context.stopService(Intent(context, BackupRestoreService::class.java))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wake lock that will be held until the service is destroyed.
|
||||||
|
*/
|
||||||
|
private lateinit var wakeLock: PowerManager.WakeLock
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscription where the update is done.
|
||||||
|
*/
|
||||||
|
private var subscription: Subscription? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The progress of a backup restore
|
||||||
|
*/
|
||||||
|
private var restoreProgress = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amount of manga in Json file (needed for restore)
|
||||||
|
*/
|
||||||
|
private var restoreAmount = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List containing errors
|
||||||
|
*/
|
||||||
|
private val errors = mutableListOf<Pair<Date, String>>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backup manager
|
||||||
|
*/
|
||||||
|
private lateinit var backupManager: BackupManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database
|
||||||
|
*/
|
||||||
|
private val db: DatabaseHelper by injectLazy()
|
||||||
|
|
||||||
|
lateinit var executor: ExecutorService
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method called when the service is created. It injects dependencies and acquire the wake lock.
|
||||||
|
*/
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
|
||||||
|
PowerManager.PARTIAL_WAKE_LOCK, "BackupRestoreService:WakeLock")
|
||||||
|
wakeLock.acquire()
|
||||||
|
executor = Executors.newSingleThreadExecutor()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method called when the service is destroyed. It destroys the running subscription and
|
||||||
|
* releases the wake lock.
|
||||||
|
*/
|
||||||
|
override fun onDestroy() {
|
||||||
|
subscription?.unsubscribe()
|
||||||
|
executor.shutdown() // must be called after unsubscribe
|
||||||
|
if (wakeLock.isHeld) {
|
||||||
|
wakeLock.release()
|
||||||
|
}
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method needs to be implemented, but it's not used/needed.
|
||||||
|
*/
|
||||||
|
override fun onBind(intent: Intent): IBinder? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method called when the service receives an intent.
|
||||||
|
*
|
||||||
|
* @param intent the start intent from.
|
||||||
|
* @param flags the flags of the command.
|
||||||
|
* @param startId the start id of this command.
|
||||||
|
* @return the start value of the command.
|
||||||
|
*/
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
if (intent == null) return Service.START_NOT_STICKY
|
||||||
|
|
||||||
|
val file = UniFile.fromUri(this, Uri.parse(intent.getStringExtra(EXTRA_URI)))
|
||||||
|
|
||||||
|
// Unsubscribe from any previous subscription if needed.
|
||||||
|
subscription?.unsubscribe()
|
||||||
|
|
||||||
|
subscription = Observable.using(
|
||||||
|
{ db.lowLevel().beginTransaction() },
|
||||||
|
{ getRestoreObservable(file).doOnNext{ db.lowLevel().setTransactionSuccessful() } },
|
||||||
|
{ executor.execute { db.lowLevel().endTransaction() } })
|
||||||
|
.doAfterTerminate { stopSelf(startId) }
|
||||||
|
.subscribeOn(Schedulers.from(executor))
|
||||||
|
.subscribe()
|
||||||
|
|
||||||
|
return Service.START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an [Observable] containing restore process.
|
||||||
|
*
|
||||||
|
* @param file restore file
|
||||||
|
* @return [Observable<Manga>]
|
||||||
|
*/
|
||||||
|
private fun getRestoreObservable(file: UniFile): Observable<List<Manga>> {
|
||||||
|
val startTime = System.currentTimeMillis()
|
||||||
|
|
||||||
|
val reader = JsonReader(file.openInputStream().bufferedReader())
|
||||||
|
val json = JsonParser().parse(reader).asJsonObject
|
||||||
|
|
||||||
|
// Get parser version
|
||||||
|
val version = json.get(VERSION)?.asInt ?: 1
|
||||||
|
|
||||||
|
// Initialize manager
|
||||||
|
backupManager = BackupManager(this, version)
|
||||||
|
|
||||||
|
val mangasJson = json.get(MANGAS).asJsonArray
|
||||||
|
|
||||||
|
restoreAmount = mangasJson.size() + 1 // +1 for categories
|
||||||
|
restoreProgress = 0
|
||||||
|
errors.clear()
|
||||||
|
|
||||||
|
// Restore categories
|
||||||
|
json.get(CATEGORIES)?.let {
|
||||||
|
backupManager.restoreCategories(it.asJsonArray)
|
||||||
|
restoreProgress += 1
|
||||||
|
showRestoreProgress(restoreProgress, restoreAmount, "Categories added", errors.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Observable.from(mangasJson)
|
||||||
|
.concatMap {
|
||||||
|
val obj = it.asJsonObject
|
||||||
|
val manga = backupManager.parser.fromJson<MangaImpl>(obj.get(MANGA))
|
||||||
|
val chapters = backupManager.parser.fromJson<List<ChapterImpl>>(obj.get(CHAPTERS) ?: JsonArray())
|
||||||
|
val categories = backupManager.parser.fromJson<List<String>>(obj.get(CATEGORIES) ?: JsonArray())
|
||||||
|
val history = backupManager.parser.fromJson<List<DHistory>>(obj.get(HISTORY) ?: JsonArray())
|
||||||
|
val tracks = backupManager.parser.fromJson<List<TrackImpl>>(obj.get(TRACK) ?: JsonArray())
|
||||||
|
|
||||||
|
val observable = getMangaRestoreObservable(manga, chapters, categories, history, tracks)
|
||||||
|
if (observable != null) {
|
||||||
|
observable
|
||||||
|
} else {
|
||||||
|
errors.add(Date() to "${manga.title} - ${getString(R.string.source_not_found)}")
|
||||||
|
restoreProgress += 1
|
||||||
|
val content = getString(R.string.dialog_restoring_source_not_found, manga.title.chop(15))
|
||||||
|
showRestoreProgress(restoreProgress, restoreAmount, manga.title, errors.size, content)
|
||||||
|
Observable.just(manga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
.doOnNext {
|
||||||
|
val endTime = System.currentTimeMillis()
|
||||||
|
val time = endTime - startTime
|
||||||
|
val logFile = writeErrorLog()
|
||||||
|
val completeIntent = Intent(SettingsBackupFragment.INTENT_FILTER).apply {
|
||||||
|
putExtra(SettingsBackupFragment.EXTRA_TIME, time)
|
||||||
|
putExtra(SettingsBackupFragment.EXTRA_ERRORS, errors.size)
|
||||||
|
putExtra(SettingsBackupFragment.EXTRA_ERROR_FILE_PATH, logFile.parent)
|
||||||
|
putExtra(SettingsBackupFragment.EXTRA_ERROR_FILE, logFile.name)
|
||||||
|
putExtra(SettingsBackupFragment.ACTION, SettingsBackupFragment.ACTION_RESTORE_COMPLETED_DIALOG)
|
||||||
|
}
|
||||||
|
sendLocalBroadcast(completeIntent)
|
||||||
|
|
||||||
|
}
|
||||||
|
.doOnError { error ->
|
||||||
|
Timber.e(error)
|
||||||
|
writeErrorLog()
|
||||||
|
val errorIntent = Intent(SettingsBackupFragment.INTENT_FILTER).apply {
|
||||||
|
putExtra(SettingsBackupFragment.ACTION, SettingsBackupFragment.ACTION_ERROR_RESTORE_DIALOG)
|
||||||
|
putExtra(SettingsBackupFragment.EXTRA_ERROR_MESSAGE, error.message)
|
||||||
|
}
|
||||||
|
sendLocalBroadcast(errorIntent)
|
||||||
|
}
|
||||||
|
.onErrorReturn { emptyList() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write errors to error log
|
||||||
|
*/
|
||||||
|
private fun writeErrorLog(): File {
|
||||||
|
try {
|
||||||
|
if (errors.isNotEmpty()) {
|
||||||
|
val destFile = File(externalCacheDir, "tachiyomi_restore.log")
|
||||||
|
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
|
||||||
|
|
||||||
|
destFile.bufferedWriter().use { out ->
|
||||||
|
errors.forEach { (date, message) ->
|
||||||
|
out.write("[${sdf.format(date)}] $message\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return destFile
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Empty
|
||||||
|
}
|
||||||
|
return File("")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a manga restore observable
|
||||||
|
*
|
||||||
|
* @param manga manga data from json
|
||||||
|
* @param chapters chapters data from json
|
||||||
|
* @param categories categories data from json
|
||||||
|
* @param history history data from json
|
||||||
|
* @param tracks tracking data from json
|
||||||
|
* @return [Observable] containing manga restore information
|
||||||
|
*/
|
||||||
|
private fun getMangaRestoreObservable(manga: Manga, chapters: List<Chapter>,
|
||||||
|
categories: List<String>, history: List<DHistory>,
|
||||||
|
tracks: List<Track>): Observable<Manga>? {
|
||||||
|
// Get source
|
||||||
|
val source = backupManager.sourceManager.get(manga.source) ?: return null
|
||||||
|
val dbManga = backupManager.getMangaFromDatabase(manga)
|
||||||
|
|
||||||
|
if (dbManga == null) {
|
||||||
|
// Manga not in database
|
||||||
|
return mangaFetchObservable(source, manga, chapters, categories, history, tracks)
|
||||||
|
} else { // Manga in database
|
||||||
|
// Copy information from manga already in database
|
||||||
|
backupManager.restoreMangaNoFetch(manga, dbManga)
|
||||||
|
// Fetch rest of manga information
|
||||||
|
return mangaNoFetchObservable(source, manga, chapters, categories, history, tracks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Observable] that fetches manga information
|
||||||
|
*
|
||||||
|
* @param manga manga that needs updating
|
||||||
|
* @param chapters chapters of manga that needs updating
|
||||||
|
* @param categories categories that need updating
|
||||||
|
*/
|
||||||
|
private fun mangaFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>,
|
||||||
|
categories: List<String>, history: List<DHistory>,
|
||||||
|
tracks: List<Track>): Observable<Manga> {
|
||||||
|
return backupManager.restoreMangaFetchObservable(source, manga)
|
||||||
|
.onErrorReturn {
|
||||||
|
errors.add(Date() to "${manga.title} - ${it.message}")
|
||||||
|
manga
|
||||||
|
}
|
||||||
|
.filter { it.id != null }
|
||||||
|
.flatMap { manga ->
|
||||||
|
chapterFetchObservable(source, manga, chapters)
|
||||||
|
// Convert to the manga that contains new chapters.
|
||||||
|
.map { manga }
|
||||||
|
}
|
||||||
|
.doOnNext {
|
||||||
|
// Restore categories
|
||||||
|
backupManager.restoreCategoriesForManga(it, categories)
|
||||||
|
|
||||||
|
// Restore history
|
||||||
|
backupManager.restoreHistoryForManga(history)
|
||||||
|
|
||||||
|
// Restore tracking
|
||||||
|
backupManager.restoreTrackForManga(it, tracks)
|
||||||
|
}
|
||||||
|
.doOnCompleted {
|
||||||
|
restoreProgress += 1
|
||||||
|
showRestoreProgress(restoreProgress, restoreAmount, manga.title, errors.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mangaNoFetchObservable(source: Source, backupManga: Manga, chapters: List<Chapter>,
|
||||||
|
categories: List<String>, history: List<DHistory>,
|
||||||
|
tracks: List<Track>): Observable<Manga> {
|
||||||
|
|
||||||
|
return Observable.just(backupManga)
|
||||||
|
.flatMap { manga ->
|
||||||
|
if (!backupManager.restoreChaptersForManga(manga, chapters)) {
|
||||||
|
chapterFetchObservable(source, manga, chapters)
|
||||||
|
.map { manga }
|
||||||
|
} else {
|
||||||
|
Observable.just(manga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.doOnNext {
|
||||||
|
// Restore categories
|
||||||
|
backupManager.restoreCategoriesForManga(it, categories)
|
||||||
|
|
||||||
|
// Restore history
|
||||||
|
backupManager.restoreHistoryForManga(history)
|
||||||
|
|
||||||
|
// Restore tracking
|
||||||
|
backupManager.restoreTrackForManga(it, tracks)
|
||||||
|
}
|
||||||
|
.doOnCompleted {
|
||||||
|
restoreProgress += 1
|
||||||
|
showRestoreProgress(restoreProgress, restoreAmount, backupManga.title, errors.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Observable] that fetches chapter information
|
||||||
|
*
|
||||||
|
* @param source source of manga
|
||||||
|
* @param manga manga that needs updating
|
||||||
|
* @return [Observable] that contains manga
|
||||||
|
*/
|
||||||
|
private fun chapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
||||||
|
return backupManager.restoreChapterFetchObservable(source, manga, chapters)
|
||||||
|
// If there's any error, return empty update and continue.
|
||||||
|
.onErrorReturn {
|
||||||
|
errors.add(Date() to "${manga.title} - ${it.message}")
|
||||||
|
Pair(emptyList<Chapter>(), emptyList<Chapter>())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to update dialog in [SettingsBackupFragment]
|
||||||
|
*
|
||||||
|
* @param progress restore progress
|
||||||
|
* @param amount total restoreAmount of manga
|
||||||
|
* @param title title of restored manga
|
||||||
|
*/
|
||||||
|
private fun showRestoreProgress(progress: Int, amount: Int, title: String, errors: Int,
|
||||||
|
content: String = getString(R.string.dialog_restoring_backup, title.chop(15))) {
|
||||||
|
val intent = Intent(SettingsBackupFragment.INTENT_FILTER).apply {
|
||||||
|
putExtra(SettingsBackupFragment.EXTRA_PROGRESS, progress)
|
||||||
|
putExtra(SettingsBackupFragment.EXTRA_AMOUNT, amount)
|
||||||
|
putExtra(SettingsBackupFragment.EXTRA_CONTENT, content)
|
||||||
|
putExtra(SettingsBackupFragment.EXTRA_ERRORS, errors)
|
||||||
|
putExtra(SettingsBackupFragment.ACTION, SettingsBackupFragment.ACTION_SET_PROGRESS_DIALOG)
|
||||||
|
}
|
||||||
|
sendLocalBroadcast(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.models
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Json values
|
||||||
|
*/
|
||||||
|
object Backup {
|
||||||
|
const val CURRENT_VERSION = 2
|
||||||
|
const val MANGA = "manga"
|
||||||
|
const val MANGAS = "mangas"
|
||||||
|
const val TRACK = "track"
|
||||||
|
const val CHAPTERS = "chapters"
|
||||||
|
const val CATEGORIES = "categories"
|
||||||
|
const val HISTORY = "history"
|
||||||
|
const val VERSION = "version"
|
||||||
|
|
||||||
|
fun getDefaultFilename(): String {
|
||||||
|
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
|
||||||
|
return "tachiyomi_$date.json"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.models
|
||||||
|
|
||||||
|
data class DHistory(val url: String,val lastRead: Long)
|
@ -1,16 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.serializer
|
|
||||||
|
|
||||||
import com.google.gson.JsonElement
|
|
||||||
import com.google.gson.JsonPrimitive
|
|
||||||
import com.google.gson.JsonSerializationContext
|
|
||||||
import com.google.gson.JsonSerializer
|
|
||||||
import java.lang.reflect.Type
|
|
||||||
|
|
||||||
class BooleanSerializer : JsonSerializer<Boolean> {
|
|
||||||
|
|
||||||
override fun serialize(value: Boolean?, type: Type, context: JsonSerializationContext): JsonElement? {
|
|
||||||
if (value != null && value != false)
|
|
||||||
return JsonPrimitive(value)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,31 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.serializer
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
|
import com.google.gson.TypeAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [CategoryImpl] to / from json
|
||||||
|
*/
|
||||||
|
object CategoryTypeAdapter {
|
||||||
|
|
||||||
|
fun build(): TypeAdapter<CategoryImpl> {
|
||||||
|
return typeAdapter {
|
||||||
|
write {
|
||||||
|
beginArray()
|
||||||
|
value(it.name)
|
||||||
|
value(it.order)
|
||||||
|
endArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
read {
|
||||||
|
beginArray()
|
||||||
|
val category = CategoryImpl()
|
||||||
|
category.name = nextString()
|
||||||
|
category.order = nextInt()
|
||||||
|
endArray()
|
||||||
|
category
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.serializer
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
|
import com.google.gson.TypeAdapter
|
||||||
|
import com.google.gson.stream.JsonToken
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [ChapterImpl] to / from json
|
||||||
|
*/
|
||||||
|
object ChapterTypeAdapter {
|
||||||
|
|
||||||
|
private const val URL = "u"
|
||||||
|
private const val READ = "r"
|
||||||
|
private const val BOOKMARK = "b"
|
||||||
|
private const val LAST_READ = "l"
|
||||||
|
|
||||||
|
fun build(): TypeAdapter<ChapterImpl> {
|
||||||
|
return typeAdapter {
|
||||||
|
write {
|
||||||
|
if (it.read || it.bookmark || it.last_page_read != 0) {
|
||||||
|
beginObject()
|
||||||
|
name(URL)
|
||||||
|
value(it.url)
|
||||||
|
if (it.read) {
|
||||||
|
name(READ)
|
||||||
|
value(1)
|
||||||
|
}
|
||||||
|
if (it.bookmark) {
|
||||||
|
name(BOOKMARK)
|
||||||
|
value(1)
|
||||||
|
}
|
||||||
|
if (it.last_page_read != 0) {
|
||||||
|
name(LAST_READ)
|
||||||
|
value(it.last_page_read)
|
||||||
|
}
|
||||||
|
endObject()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
read {
|
||||||
|
val chapter = ChapterImpl()
|
||||||
|
beginObject()
|
||||||
|
while (hasNext()) {
|
||||||
|
if (peek() == JsonToken.NAME) {
|
||||||
|
val name = nextName()
|
||||||
|
|
||||||
|
when (name) {
|
||||||
|
URL -> chapter.url = nextString()
|
||||||
|
READ -> chapter.read = nextInt() == 1
|
||||||
|
BOOKMARK -> chapter.bookmark = nextInt() == 1
|
||||||
|
LAST_READ -> chapter.last_page_read = nextInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
endObject()
|
||||||
|
chapter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.serializer
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
|
import com.google.gson.TypeAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [DHistory] to / from json
|
||||||
|
*/
|
||||||
|
object HistoryTypeAdapter {
|
||||||
|
|
||||||
|
fun build(): TypeAdapter<DHistory> {
|
||||||
|
return typeAdapter {
|
||||||
|
write {
|
||||||
|
if (it.lastRead != 0L) {
|
||||||
|
beginArray()
|
||||||
|
value(it.url)
|
||||||
|
value(it.lastRead)
|
||||||
|
endArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
read {
|
||||||
|
beginArray()
|
||||||
|
val url = nextString()
|
||||||
|
val lastRead = nextLong()
|
||||||
|
endArray()
|
||||||
|
DHistory(url, lastRead)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,27 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.serializer
|
|
||||||
|
|
||||||
import com.google.gson.ExclusionStrategy
|
|
||||||
import com.google.gson.FieldAttributes
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
|
||||||
|
|
||||||
class IdExclusion : ExclusionStrategy {
|
|
||||||
|
|
||||||
private val categoryExclusions = listOf("id")
|
|
||||||
private val mangaExclusions = listOf("id")
|
|
||||||
private val chapterExclusions = listOf("id", "manga_id")
|
|
||||||
private val syncExclusions = listOf("id", "manga_id", "update")
|
|
||||||
|
|
||||||
override fun shouldSkipField(f: FieldAttributes) = when (f.declaringClass) {
|
|
||||||
MangaImpl::class.java -> mangaExclusions.contains(f.name)
|
|
||||||
ChapterImpl::class.java -> chapterExclusions.contains(f.name)
|
|
||||||
TrackImpl::class.java -> syncExclusions.contains(f.name)
|
|
||||||
CategoryImpl::class.java -> categoryExclusions.contains(f.name)
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun shouldSkipClass(clazz: Class<*>) = false
|
|
||||||
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.serializer
|
|
||||||
|
|
||||||
import com.google.gson.JsonElement
|
|
||||||
import com.google.gson.JsonPrimitive
|
|
||||||
import com.google.gson.JsonSerializationContext
|
|
||||||
import com.google.gson.JsonSerializer
|
|
||||||
|
|
||||||
import java.lang.reflect.Type
|
|
||||||
|
|
||||||
class IntegerSerializer : JsonSerializer<Int> {
|
|
||||||
|
|
||||||
override fun serialize(value: Int?, type: Type, context: JsonSerializationContext): JsonElement? {
|
|
||||||
if (value != null && value !== 0)
|
|
||||||
return JsonPrimitive(value)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.serializer
|
|
||||||
|
|
||||||
import com.google.gson.JsonElement
|
|
||||||
import com.google.gson.JsonPrimitive
|
|
||||||
import com.google.gson.JsonSerializationContext
|
|
||||||
import com.google.gson.JsonSerializer
|
|
||||||
import java.lang.reflect.Type
|
|
||||||
|
|
||||||
class LongSerializer : JsonSerializer<Long> {
|
|
||||||
|
|
||||||
override fun serialize(value: Long?, type: Type, context: JsonSerializationContext): JsonElement? {
|
|
||||||
if (value != null && value !== 0L)
|
|
||||||
return JsonPrimitive(value)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,37 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.serializer
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
|
import com.google.gson.TypeAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [MangaImpl] to / from json
|
||||||
|
*/
|
||||||
|
object MangaTypeAdapter {
|
||||||
|
|
||||||
|
fun build(): TypeAdapter<MangaImpl> {
|
||||||
|
return typeAdapter {
|
||||||
|
write {
|
||||||
|
beginArray()
|
||||||
|
value(it.url)
|
||||||
|
value(it.title)
|
||||||
|
value(it.source)
|
||||||
|
value(it.viewer)
|
||||||
|
value(it.chapter_flags)
|
||||||
|
endArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
read {
|
||||||
|
beginArray()
|
||||||
|
val manga = MangaImpl()
|
||||||
|
manga.url = nextString()
|
||||||
|
manga.title = nextString()
|
||||||
|
manga.source = nextLong()
|
||||||
|
manga.viewer = nextInt()
|
||||||
|
manga.chapter_flags = nextInt()
|
||||||
|
endArray()
|
||||||
|
manga
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.serializer
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
|
import com.google.gson.TypeAdapter
|
||||||
|
import com.google.gson.stream.JsonToken
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [TrackImpl] to / from json
|
||||||
|
*/
|
||||||
|
object TrackTypeAdapter {
|
||||||
|
|
||||||
|
private const val SYNC = "s"
|
||||||
|
private const val REMOTE = "r"
|
||||||
|
private const val TITLE = "t"
|
||||||
|
private const val LAST_READ = "l"
|
||||||
|
|
||||||
|
fun build(): TypeAdapter<TrackImpl> {
|
||||||
|
return typeAdapter {
|
||||||
|
write {
|
||||||
|
beginObject()
|
||||||
|
name(TITLE)
|
||||||
|
value(it.title)
|
||||||
|
name(SYNC)
|
||||||
|
value(it.sync_id)
|
||||||
|
name(REMOTE)
|
||||||
|
value(it.remote_id)
|
||||||
|
name(LAST_READ)
|
||||||
|
value(it.last_chapter_read)
|
||||||
|
endObject()
|
||||||
|
}
|
||||||
|
|
||||||
|
read {
|
||||||
|
val track = TrackImpl()
|
||||||
|
beginObject()
|
||||||
|
while (hasNext()) {
|
||||||
|
if (peek() == JsonToken.NAME) {
|
||||||
|
val name = nextName()
|
||||||
|
|
||||||
|
when (name) {
|
||||||
|
TITLE -> track.title = nextString()
|
||||||
|
SYNC -> track.sync_id = nextInt()
|
||||||
|
REMOTE -> track.remote_id = nextInt()
|
||||||
|
LAST_READ -> track.last_chapter_read = nextInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
endObject()
|
||||||
|
track
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -24,4 +24,6 @@ open class DatabaseHelper(context: Context)
|
|||||||
|
|
||||||
inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)
|
inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)
|
||||||
|
|
||||||
|
fun lowLevel() = db.lowLevel()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
|||||||
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
||||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||||
import eu.kanade.tachiyomi.data.database.models.History
|
import eu.kanade.tachiyomi.data.database.models.History
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.HistoryImpl
|
||||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_CHAPTER_ID
|
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_CHAPTER_ID
|
||||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_ID
|
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_ID
|
||||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_LAST_READ
|
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_LAST_READ
|
||||||
@ -44,7 +45,7 @@ open class HistoryPutResolver : DefaultPutResolver<History>() {
|
|||||||
|
|
||||||
class HistoryGetResolver : DefaultGetResolver<History>() {
|
class HistoryGetResolver : DefaultGetResolver<History>() {
|
||||||
|
|
||||||
override fun mapFromCursor(cursor: Cursor): History = History().apply {
|
override fun mapFromCursor(cursor: Cursor): History = HistoryImpl().apply {
|
||||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
||||||
chapter_id = cursor.getLong(cursor.getColumnIndex(COL_CHAPTER_ID))
|
chapter_id = cursor.getLong(cursor.getColumnIndex(COL_CHAPTER_ID))
|
||||||
last_read = cursor.getLong(cursor.getColumnIndex(COL_LAST_READ))
|
last_read = cursor.getLong(cursor.getColumnIndex(COL_LAST_READ))
|
||||||
|
@ -5,27 +5,27 @@ import java.io.Serializable
|
|||||||
/**
|
/**
|
||||||
* Object containing the history statistics of a chapter
|
* Object containing the history statistics of a chapter
|
||||||
*/
|
*/
|
||||||
class History : Serializable {
|
interface History : Serializable {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Id of history object.
|
* Id of history object.
|
||||||
*/
|
*/
|
||||||
var id: Long? = null
|
var id: Long?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Chapter id of history object.
|
* Chapter id of history object.
|
||||||
*/
|
*/
|
||||||
var chapter_id: Long = 0
|
var chapter_id: Long
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Last time chapter was read in time long format
|
* Last time chapter was read in time long format
|
||||||
*/
|
*/
|
||||||
var last_read: Long = 0
|
var last_read: Long
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Total time chapter was read - todo not yet implemented
|
* Total time chapter was read - todo not yet implemented
|
||||||
*/
|
*/
|
||||||
var time_read: Long = 0
|
var time_read: Long
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
@ -35,10 +35,8 @@ class History : Serializable {
|
|||||||
* @param chapter chapter object
|
* @param chapter chapter object
|
||||||
* @return history object
|
* @return history object
|
||||||
*/
|
*/
|
||||||
fun create(chapter: Chapter): History {
|
fun create(chapter: Chapter): History = HistoryImpl().apply {
|
||||||
val history = History()
|
this.chapter_id = chapter.id!!
|
||||||
history.chapter_id = chapter.id!!
|
|
||||||
return history
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object containing the history statistics of a chapter
|
||||||
|
*/
|
||||||
|
class HistoryImpl : History {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Id of history object.
|
||||||
|
*/
|
||||||
|
override var id: Long? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chapter id of history object.
|
||||||
|
*/
|
||||||
|
override var chapter_id: Long = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Last time chapter was read in time long format
|
||||||
|
*/
|
||||||
|
override var last_read: Long = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Total time chapter was read - todo not yet implemented
|
||||||
|
*/
|
||||||
|
override var time_read: Long = 0
|
||||||
|
}
|
@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.data.database.DbProvider
|
|||||||
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.database.models.MangaChapter
|
import eu.kanade.tachiyomi.data.database.models.MangaChapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.resolvers.ChapterBackupPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.ChapterProgressPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.ChapterProgressPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.ChapterSourceOrderPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.ChapterSourceOrderPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver
|
||||||
@ -42,6 +43,16 @@ interface ChapterQueries : DbProvider {
|
|||||||
.build())
|
.build())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun getChapter(url: String) = db.get()
|
||||||
|
.`object`(Chapter::class.java)
|
||||||
|
.withQuery(Query.builder()
|
||||||
|
.table(ChapterTable.TABLE)
|
||||||
|
.where("${ChapterTable.COL_URL} = ?")
|
||||||
|
.whereArgs(url)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
|
||||||
fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare()
|
fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare()
|
||||||
|
|
||||||
fun insertChapters(chapters: List<Chapter>) = db.put().objects(chapters).prepare()
|
fun insertChapters(chapters: List<Chapter>) = db.put().objects(chapters).prepare()
|
||||||
@ -50,6 +61,11 @@ interface ChapterQueries : DbProvider {
|
|||||||
|
|
||||||
fun deleteChapters(chapters: List<Chapter>) = db.delete().objects(chapters).prepare()
|
fun deleteChapters(chapters: List<Chapter>) = db.delete().objects(chapters).prepare()
|
||||||
|
|
||||||
|
fun updateChaptersBackup(chapters: List<Chapter>) = db.put()
|
||||||
|
.objects(chapters)
|
||||||
|
.withPutResolver(ChapterBackupPutResolver())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun updateChapterProgress(chapter: Chapter) = db.put()
|
fun updateChapterProgress(chapter: Chapter) = db.put()
|
||||||
.`object`(chapter)
|
.`object`(chapter)
|
||||||
.withPutResolver(ChapterProgressPutResolver())
|
.withPutResolver(ChapterProgressPutResolver())
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.queries
|
package eu.kanade.tachiyomi.data.database.queries
|
||||||
|
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||||
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
||||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||||
import eu.kanade.tachiyomi.data.database.models.History
|
import eu.kanade.tachiyomi.data.database.models.History
|
||||||
@ -40,6 +41,15 @@ interface HistoryQueries : DbProvider {
|
|||||||
.build())
|
.build())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun getHistoryByChapterUrl(chapterUrl: String) = db.get()
|
||||||
|
.`object`(History::class.java)
|
||||||
|
.withQuery(RawQuery.builder()
|
||||||
|
.query(getHistoryByChapterUrl())
|
||||||
|
.args(chapterUrl)
|
||||||
|
.observesTables(HistoryTable.TABLE)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the history last read.
|
* Updates the history last read.
|
||||||
* Inserts history object if not yet in database
|
* Inserts history object if not yet in database
|
||||||
@ -59,4 +69,18 @@ interface HistoryQueries : DbProvider {
|
|||||||
.objects(historyList)
|
.objects(historyList)
|
||||||
.withPutResolver(HistoryLastReadPutResolver())
|
.withPutResolver(HistoryLastReadPutResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun deleteHistory() = db.delete()
|
||||||
|
.byQuery(DeleteQuery.builder()
|
||||||
|
.table(HistoryTable.TABLE)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
fun deleteHistoryNoLastRead() = db.delete()
|
||||||
|
.byQuery(DeleteQuery.builder()
|
||||||
|
.table(HistoryTable.TABLE)
|
||||||
|
.where("${HistoryTable.COL_LAST_READ} = ?")
|
||||||
|
.whereArgs(0)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
}
|
}
|
||||||
|
@ -84,6 +84,12 @@ interface MangaQueries : DbProvider {
|
|||||||
.build())
|
.build())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun deleteMangas() = db.delete()
|
||||||
|
.byQuery(DeleteQuery.builder()
|
||||||
|
.table(MangaTable.TABLE)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun getLastReadManga() = db.get()
|
fun getLastReadManga() = db.get()
|
||||||
.listOfObjects(Manga::class.java)
|
.listOfObjects(Manga::class.java)
|
||||||
.withQuery(RawQuery.builder()
|
.withQuery(RawQuery.builder()
|
||||||
|
@ -73,6 +73,14 @@ fun getHistoryByMangaId() = """
|
|||||||
WHERE ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ? AND ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
|
WHERE ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ? AND ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
fun getHistoryByChapterUrl() = """
|
||||||
|
SELECT ${History.TABLE}.*
|
||||||
|
FROM ${History.TABLE}
|
||||||
|
JOIN ${Chapter.TABLE}
|
||||||
|
ON ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
|
||||||
|
WHERE ${Chapter.TABLE}.${Chapter.COL_URL} = ? AND ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
|
||||||
|
"""
|
||||||
|
|
||||||
fun getLastReadMangaQuery() = """
|
fun getLastReadMangaQuery() = """
|
||||||
SELECT ${Manga.TABLE}.*, MAX(${History.TABLE}.${History.COL_LAST_READ}) AS max
|
SELECT ${Manga.TABLE}.*, MAX(${History.TABLE}.${History.COL_LAST_READ}) AS max
|
||||||
FROM ${Manga.TABLE}
|
FROM ${Manga.TABLE}
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.resolvers
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||||
|
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||||
|
|
||||||
|
class ChapterBackupPutResolver : PutResolver<Chapter>() {
|
||||||
|
|
||||||
|
override fun performPut(db: StorIOSQLite, chapter: Chapter) = db.inTransactionReturn {
|
||||||
|
val updateQuery = mapToUpdateQuery(chapter)
|
||||||
|
val contentValues = mapToContentValues(chapter)
|
||||||
|
|
||||||
|
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||||
|
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
|
||||||
|
.table(ChapterTable.TABLE)
|
||||||
|
.where("${ChapterTable.COL_URL} = ?")
|
||||||
|
.whereArgs(chapter.url)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply {
|
||||||
|
put(ChapterTable.COL_READ, chapter.read)
|
||||||
|
put(ChapterTable.COL_BOOKMARK, chapter.bookmark)
|
||||||
|
put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.data.notification.NotificationHandler
|
|||||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
import eu.kanade.tachiyomi.util.chop
|
import eu.kanade.tachiyomi.util.chop
|
||||||
import eu.kanade.tachiyomi.util.notificationManager
|
import eu.kanade.tachiyomi.util.notificationManager
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DownloadNotifier is used to show notifications when downloading one or multiple chapters.
|
* DownloadNotifier is used to show notifications when downloading one or multiple chapters.
|
||||||
@ -145,7 +146,8 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
} else {
|
} else {
|
||||||
download?.let {
|
download?.let {
|
||||||
val title = it.manga.title.chop(15)
|
val title = it.manga.title.chop(15)
|
||||||
val chapter = download.chapter.name.replaceFirst("$title[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), "")
|
val quotedTitle = Pattern.quote(title)
|
||||||
|
val chapter = download.chapter.name.replaceFirst("$quotedTitle[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), "")
|
||||||
setContentTitle("$title - $chapter".chop(30))
|
setContentTitle("$title - $chapter".chop(30))
|
||||||
setContentText(context.getString(R.string.chapter_downloading_progress)
|
setContentText(context.getString(R.string.chapter_downloading_progress)
|
||||||
.format(it.downloadedImages, it.pages!!.size))
|
.format(it.downloadedImages, it.pages!!.size))
|
||||||
@ -202,7 +204,8 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
// Create notification.
|
// Create notification.
|
||||||
with(notification) {
|
with(notification) {
|
||||||
val title = download.manga.title.chop(15)
|
val title = download.manga.title.chop(15)
|
||||||
val chapter = download.chapter.name.replaceFirst("$title[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), "")
|
val quotedTitle = Pattern.quote(title)
|
||||||
|
val chapter = download.chapter.name.replaceFirst("$quotedTitle[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), "")
|
||||||
setContentTitle("$title - $chapter".chop(30))
|
setContentTitle("$title - $chapter".chop(30))
|
||||||
setContentText(context.getString(R.string.update_check_notification_download_complete))
|
setContentText(context.getString(R.string.update_check_notification_download_complete))
|
||||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
package eu.kanade.tachiyomi.data.library
|
package eu.kanade.tachiyomi.data.library
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
|
import android.os.Build
|
||||||
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
|
||||||
@ -28,7 +30,9 @@ import eu.kanade.tachiyomi.util.*
|
|||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import uy.kohesive.injekt.injectLazy
|
import timber.log.Timber
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
@ -40,24 +44,12 @@ import java.util.concurrent.atomic.AtomicInteger
|
|||||||
* progress of the update, and if case of an unexpected error, this service will be silently
|
* progress of the update, and if case of an unexpected error, this service will be silently
|
||||||
* destroyed.
|
* destroyed.
|
||||||
*/
|
*/
|
||||||
class LibraryUpdateService : Service() {
|
class LibraryUpdateService(
|
||||||
|
val db: DatabaseHelper = Injekt.get(),
|
||||||
/**
|
val sourceManager: SourceManager = Injekt.get(),
|
||||||
* Database helper.
|
val preferences: PreferencesHelper = Injekt.get(),
|
||||||
*/
|
val downloadManager: DownloadManager = Injekt.get()
|
||||||
val db: DatabaseHelper by injectLazy()
|
) : Service() {
|
||||||
|
|
||||||
/**
|
|
||||||
* Source manager.
|
|
||||||
*/
|
|
||||||
val sourceManager: SourceManager by injectLazy()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preferences.
|
|
||||||
*/
|
|
||||||
val preferences: PreferencesHelper by injectLazy()
|
|
||||||
|
|
||||||
val downloadManager: DownloadManager by injectLazy()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wake lock that will be held until the service is destroyed.
|
* Wake lock that will be held until the service is destroyed.
|
||||||
@ -72,18 +64,27 @@ class LibraryUpdateService : Service() {
|
|||||||
/**
|
/**
|
||||||
* Pending intent of action that cancels the library update
|
* Pending intent of action that cancels the library update
|
||||||
*/
|
*/
|
||||||
private val cancelPendingIntent by lazy {NotificationReceiver.cancelLibraryUpdatePendingBroadcast(this)}
|
private val cancelIntent by lazy {
|
||||||
|
NotificationReceiver.cancelLibraryUpdatePendingBroadcast(this)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Id of the library update notification.
|
* Bitmap of the app for notifications.
|
||||||
*/
|
*/
|
||||||
private val notificationId: Int
|
|
||||||
get() = Constants.NOTIFICATION_LIBRARY_ID
|
|
||||||
|
|
||||||
private val notificationBitmap by lazy {
|
private val notificationBitmap by lazy {
|
||||||
BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)
|
BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cached progress notification to avoid creating a lot.
|
||||||
|
*/
|
||||||
|
private val progressNotification by lazy { NotificationCompat.Builder(this)
|
||||||
|
.setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
||||||
|
.setLargeIcon(notificationBitmap)
|
||||||
|
.setOngoing(true)
|
||||||
|
.addAction(R.drawable.ic_clear_grey_24dp_img, getString(android.R.string.cancel), cancelIntent)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -141,16 +142,20 @@ class LibraryUpdateService : Service() {
|
|||||||
*/
|
*/
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
createAndAcquireWakeLock()
|
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
|
||||||
|
PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock")
|
||||||
|
wakeLock.acquire()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method called when the service is destroyed. It destroys the running subscription, resets
|
* Method called when the service is destroyed. It destroys subscriptions and releases the wake
|
||||||
* the alarm and release the wake lock.
|
* lock.
|
||||||
*/
|
*/
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
subscription?.unsubscribe()
|
subscription?.unsubscribe()
|
||||||
destroyWakeLock()
|
if (wakeLock.isHeld) {
|
||||||
|
wakeLock.release()
|
||||||
|
}
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,7 +194,7 @@ class LibraryUpdateService : Service() {
|
|||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.subscribe({
|
.subscribe({
|
||||||
}, {
|
}, {
|
||||||
showNotification(getString(R.string.notification_update_error), "")
|
Timber.e(it)
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
}, {
|
}, {
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
@ -210,13 +215,13 @@ class LibraryUpdateService : Service() {
|
|||||||
var listToUpdate = if (categoryId != -1)
|
var listToUpdate = if (categoryId != -1)
|
||||||
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
|
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
|
||||||
else {
|
else {
|
||||||
val categoriesToUpdate = preferences.libraryUpdateCategories().getOrDefault().map { it.toInt() }
|
val categoriesToUpdate = preferences.libraryUpdateCategories().getOrDefault().map(String::toInt)
|
||||||
if (categoriesToUpdate.isNotEmpty())
|
if (categoriesToUpdate.isNotEmpty())
|
||||||
db.getLibraryMangas().executeAsBlocking()
|
db.getLibraryMangas().executeAsBlocking()
|
||||||
.filter { it.category in categoriesToUpdate }
|
.filter { it.category in categoriesToUpdate }
|
||||||
.distinctBy { it.id }
|
.distinctBy { it.id }
|
||||||
else
|
else
|
||||||
db.getFavoriteMangas().executeAsBlocking().distinctBy { it.id }
|
db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!intent.getBooleanExtra(UPDATE_DETAILS, false) && preferences.updateOnlyNonCompleted()) {
|
if (!intent.getBooleanExtra(UPDATE_DETAILS, false) && preferences.updateOnlyNonCompleted()) {
|
||||||
@ -238,13 +243,21 @@ class LibraryUpdateService : Service() {
|
|||||||
fun updateChapterList(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)
|
||||||
|
// List containing new updates
|
||||||
val newUpdates = ArrayList<Manga>()
|
val newUpdates = ArrayList<Manga>()
|
||||||
|
// list containing failed updates
|
||||||
val failedUpdates = ArrayList<Manga>()
|
val failedUpdates = ArrayList<Manga>()
|
||||||
|
// List containing categories that get included in downloads.
|
||||||
|
val categoriesToDownload = preferences.downloadNewCategories().getOrDefault().map(String::toInt)
|
||||||
|
// Boolean to determine if user wants to automatically download new chapters.
|
||||||
|
val downloadNew = preferences.downloadNew().getOrDefault()
|
||||||
|
// Boolean to determine if DownloadManager has downloads
|
||||||
|
var hasDownloads = false
|
||||||
|
|
||||||
// Emit each manga and update it sequentially.
|
// Emit each manga and update it sequentially.
|
||||||
return Observable.from(mangaToUpdate)
|
return Observable.from(mangaToUpdate)
|
||||||
// Notify manga that will update.
|
// Notify manga that will update.
|
||||||
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size, cancelPendingIntent) }
|
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) }
|
||||||
// Update the chapters of the manga.
|
// Update the chapters of the manga.
|
||||||
.concatMap { manga ->
|
.concatMap { manga ->
|
||||||
updateManga(manga)
|
updateManga(manga)
|
||||||
@ -254,10 +267,13 @@ class LibraryUpdateService : Service() {
|
|||||||
Pair(emptyList<Chapter>(), emptyList<Chapter>())
|
Pair(emptyList<Chapter>(), emptyList<Chapter>())
|
||||||
}
|
}
|
||||||
// Filter out mangas without new chapters (or failed).
|
// Filter out mangas without new chapters (or failed).
|
||||||
.filter { pair -> pair.first.size > 0 }
|
.filter { pair -> pair.first.isNotEmpty() }
|
||||||
.doOnNext {
|
.doOnNext {
|
||||||
if (preferences.downloadNew()) {
|
if (downloadNew && (categoriesToDownload.isEmpty() ||
|
||||||
|
manga.category in categoriesToDownload)) {
|
||||||
|
|
||||||
downloadChapters(manga, it.first)
|
downloadChapters(manga, it.first)
|
||||||
|
hasDownloads = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Convert to the manga that contains new chapters.
|
// Convert to the manga that contains new chapters.
|
||||||
@ -273,14 +289,18 @@ class LibraryUpdateService : Service() {
|
|||||||
}
|
}
|
||||||
// Notify result of the overall update.
|
// Notify result of the overall update.
|
||||||
.doOnCompleted {
|
.doOnCompleted {
|
||||||
if (newUpdates.isEmpty()) {
|
if (newUpdates.isNotEmpty()) {
|
||||||
cancelNotification()
|
showResultNotification(newUpdates)
|
||||||
} else {
|
if (downloadNew && hasDownloads) {
|
||||||
if (preferences.downloadNew()) {
|
|
||||||
DownloadService.start(this)
|
DownloadService.start(this)
|
||||||
}
|
}
|
||||||
showResultNotification(newUpdates, failedUpdates)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (failedUpdates.isNotEmpty()) {
|
||||||
|
Timber.e("Failed updating: ${failedUpdates.map { it.title }}")
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelProgressNotification()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -321,7 +341,7 @@ class LibraryUpdateService : Service() {
|
|||||||
// Emit each manga and update it sequentially.
|
// Emit each manga and update it sequentially.
|
||||||
return Observable.from(mangaToUpdate)
|
return Observable.from(mangaToUpdate)
|
||||||
// Notify manga that will update.
|
// Notify manga that will update.
|
||||||
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size, cancelPendingIntent) }
|
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) }
|
||||||
// Update the details of the manga.
|
// Update the details of the manga.
|
||||||
.concatMap { manga ->
|
.concatMap { manga ->
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource
|
val source = sourceManager.get(manga.source) as? HttpSource
|
||||||
@ -336,73 +356,10 @@ class LibraryUpdateService : Service() {
|
|||||||
.onErrorReturn { manga }
|
.onErrorReturn { manga }
|
||||||
}
|
}
|
||||||
.doOnCompleted {
|
.doOnCompleted {
|
||||||
cancelNotification()
|
cancelProgressNotification()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the text that will be displayed in the notification when there are new chapters.
|
|
||||||
*
|
|
||||||
* @param updates a list of manga that contains new chapters.
|
|
||||||
* @param failedUpdates a list of manga that failed to update.
|
|
||||||
* @return the body of the notification to display.
|
|
||||||
*/
|
|
||||||
private fun getUpdatedMangasBody(updates: List<Manga>, failedUpdates: List<Manga>): String {
|
|
||||||
return buildString {
|
|
||||||
if (updates.isEmpty()) {
|
|
||||||
append(getString(R.string.notification_no_new_chapters))
|
|
||||||
append("\n")
|
|
||||||
} else {
|
|
||||||
append(getString(R.string.notification_new_chapters))
|
|
||||||
for (manga in updates) {
|
|
||||||
append("\n")
|
|
||||||
append(manga.title.chop(45))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!failedUpdates.isEmpty()) {
|
|
||||||
append("\n\n")
|
|
||||||
append(getString(R.string.notification_manga_update_failed))
|
|
||||||
for (manga in failedUpdates) {
|
|
||||||
append("\n")
|
|
||||||
append(manga.title.chop(45))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates and acquires a wake lock until the library is updated.
|
|
||||||
*/
|
|
||||||
private fun createAndAcquireWakeLock() {
|
|
||||||
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
|
|
||||||
PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock")
|
|
||||||
wakeLock.acquire()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Releases the wake lock if it's held.
|
|
||||||
*/
|
|
||||||
private fun destroyWakeLock() {
|
|
||||||
if (wakeLock.isHeld) {
|
|
||||||
wakeLock.release()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows the notification with the given title and body.
|
|
||||||
*
|
|
||||||
* @param title the title of the notification.
|
|
||||||
* @param body the body of the notification.
|
|
||||||
*/
|
|
||||||
private fun showNotification(title: String, body: String) {
|
|
||||||
notificationManager.notify(notificationId, notification {
|
|
||||||
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
|
||||||
setLargeIcon(notificationBitmap)
|
|
||||||
setContentTitle(title)
|
|
||||||
setContentText(body)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows the notification containing the currently updating manga and the progress.
|
* Shows the notification containing the currently updating manga and the progress.
|
||||||
*
|
*
|
||||||
@ -410,52 +367,66 @@ class LibraryUpdateService : Service() {
|
|||||||
* @param current the current progress.
|
* @param current the current progress.
|
||||||
* @param total the total progress.
|
* @param total the total progress.
|
||||||
*/
|
*/
|
||||||
private fun showProgressNotification(manga: Manga, current: Int, total: Int, cancelIntent: PendingIntent) {
|
private fun showProgressNotification(manga: Manga, current: Int, total: Int) {
|
||||||
notificationManager.notify(notificationId, notification {
|
notificationManager.notify(Constants.NOTIFICATION_LIBRARY_PROGRESS_ID, progressNotification
|
||||||
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
.setContentTitle(manga.title)
|
||||||
setLargeIcon(notificationBitmap)
|
.setProgress(total, current, false)
|
||||||
setContentTitle(manga.title)
|
.build())
|
||||||
setProgress(total, current, false)
|
|
||||||
setOngoing(true)
|
|
||||||
addAction(R.drawable.ic_clear_grey_24dp_img, getString(android.R.string.cancel), cancelIntent)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows the notification containing the result of the update done by the service.
|
* Shows the notification containing the result of the update done by the service.
|
||||||
*
|
*
|
||||||
* @param updates a list of manga with new updates.
|
* @param updates a list of manga with new updates.
|
||||||
* @param failed a list of manga that failed to update.
|
|
||||||
*/
|
*/
|
||||||
private fun showResultNotification(updates: List<Manga>, failed: List<Manga>) {
|
private fun showResultNotification(updates: List<Manga>) {
|
||||||
val title = getString(R.string.notification_update_completed)
|
val newUpdates = updates.map { it.title.chop(45) }.toMutableSet()
|
||||||
val body = getUpdatedMangasBody(updates, failed)
|
|
||||||
|
|
||||||
notificationManager.notify(notificationId, notification {
|
// Append new chapters from a previous, existing notification
|
||||||
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
val previousNotification = notificationManager.activeNotifications
|
||||||
|
.find { it.id == Constants.NOTIFICATION_LIBRARY_RESULT_ID }
|
||||||
|
|
||||||
|
if (previousNotification != null) {
|
||||||
|
val oldUpdates = previousNotification.notification.extras
|
||||||
|
.getString(Notification.EXTRA_BIG_TEXT)
|
||||||
|
|
||||||
|
if (!oldUpdates.isNullOrEmpty()) {
|
||||||
|
newUpdates += oldUpdates.split("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationManager.notify(Constants.NOTIFICATION_LIBRARY_RESULT_ID, notification {
|
||||||
|
setSmallIcon(R.drawable.ic_book_white_24dp)
|
||||||
setLargeIcon(notificationBitmap)
|
setLargeIcon(notificationBitmap)
|
||||||
setContentTitle(title)
|
setContentTitle(getString(R.string.notification_new_chapters))
|
||||||
setStyle(NotificationCompat.BigTextStyle().bigText(body))
|
if (newUpdates.size > 1) {
|
||||||
setContentIntent(notificationIntent)
|
setContentText(getString(R.string.notification_new_chapters_text, newUpdates.size))
|
||||||
|
setStyle(NotificationCompat.BigTextStyle().bigText(newUpdates.joinToString("\n")))
|
||||||
|
} else {
|
||||||
|
setContentText(newUpdates.first())
|
||||||
|
}
|
||||||
|
priority = NotificationCompat.PRIORITY_HIGH
|
||||||
|
setContentIntent(getNotificationIntent())
|
||||||
setAutoCancel(true)
|
setAutoCancel(true)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cancels the notification.
|
* Cancels the progress notification.
|
||||||
*/
|
*/
|
||||||
private fun cancelNotification() {
|
private fun cancelProgressNotification() {
|
||||||
notificationManager.cancel(notificationId)
|
notificationManager.cancel(Constants.NOTIFICATION_LIBRARY_PROGRESS_ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Property that returns an intent to open the main activity.
|
* Returns an intent to open the main activity.
|
||||||
*/
|
*/
|
||||||
private val notificationIntent: PendingIntent
|
private fun getNotificationIntent(): PendingIntent {
|
||||||
get() {
|
val intent = Intent(this, MainActivity::class.java)
|
||||||
val intent = Intent(this, MainActivity::class.java)
|
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,6 @@ package eu.kanade.tachiyomi.data.notification
|
|||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.support.v4.content.FileProvider
|
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
|
||||||
import eu.kanade.tachiyomi.ui.download.DownloadActivity
|
import eu.kanade.tachiyomi.ui.download.DownloadActivity
|
||||||
import eu.kanade.tachiyomi.util.getUriCompat
|
import eu.kanade.tachiyomi.util.getUriCompat
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -33,7 +31,7 @@ object NotificationHandler {
|
|||||||
*/
|
*/
|
||||||
internal fun openImagePendingActivity(context: Context, file: File): PendingIntent {
|
internal fun openImagePendingActivity(context: Context, file: File): PendingIntent {
|
||||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
val uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file)
|
val uri = file.getUriCompat(context)
|
||||||
setDataAndType(uri, "image/*")
|
setDataAndType(uri, "image/*")
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.data.download.DownloadManager
|
|||||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||||
import eu.kanade.tachiyomi.util.deleteIfExists
|
import eu.kanade.tachiyomi.util.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.getUriCompat
|
import eu.kanade.tachiyomi.util.getUriCompat
|
||||||
import eu.kanade.tachiyomi.util.notificationManager
|
import eu.kanade.tachiyomi.util.notificationManager
|
||||||
import eu.kanade.tachiyomi.util.toast
|
import eu.kanade.tachiyomi.util.toast
|
||||||
@ -48,7 +48,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
ACTION_DELETE_IMAGE -> deleteImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION),
|
ACTION_DELETE_IMAGE -> deleteImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION),
|
||||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
|
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
|
||||||
// Cancel library update and dismiss notification
|
// Cancel library update and dismiss notification
|
||||||
ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context, Constants.NOTIFICATION_LIBRARY_ID)
|
ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context, Constants.NOTIFICATION_LIBRARY_PROGRESS_ID)
|
||||||
// Open reader activity
|
// Open reader activity
|
||||||
ACTION_OPEN_CHAPTER -> {
|
ACTION_OPEN_CHAPTER -> {
|
||||||
openChapter(context, intent.getLongExtra(EXTRA_MANGA_ID, -1),
|
openChapter(context, intent.getLongExtra(EXTRA_MANGA_ID, -1),
|
||||||
@ -120,7 +120,10 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
dismissNotification(context, notificationId)
|
dismissNotification(context, notificationId)
|
||||||
|
|
||||||
// Delete file
|
// Delete file
|
||||||
File(path).deleteIfExists()
|
val file = File(path)
|
||||||
|
file.delete()
|
||||||
|
|
||||||
|
DiskUtil.scanMedia(context, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -180,7 +183,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
||||||
action = ACTION_RESUME_DOWNLOADS
|
action = ACTION_RESUME_DOWNLOADS
|
||||||
}
|
}
|
||||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
|
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -193,7 +196,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
||||||
action = ACTION_CLEAR_DOWNLOADS
|
action = ACTION_CLEAR_DOWNLOADS
|
||||||
}
|
}
|
||||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
|
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -208,7 +211,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
action = ACTION_DISMISS_NOTIFICATION
|
action = ACTION_DISMISS_NOTIFICATION
|
||||||
putExtra(EXTRA_NOTIFICATION_ID, notificationId)
|
putExtra(EXTRA_NOTIFICATION_ID, notificationId)
|
||||||
}
|
}
|
||||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
|
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -225,7 +228,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
putExtra(EXTRA_FILE_LOCATION, path)
|
putExtra(EXTRA_FILE_LOCATION, path)
|
||||||
putExtra(EXTRA_NOTIFICATION_ID, notificationId)
|
putExtra(EXTRA_NOTIFICATION_ID, notificationId)
|
||||||
}
|
}
|
||||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
|
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -242,7 +245,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
putExtra(EXTRA_FILE_LOCATION, path)
|
putExtra(EXTRA_FILE_LOCATION, path)
|
||||||
putExtra(EXTRA_NOTIFICATION_ID, notificationId)
|
putExtra(EXTRA_NOTIFICATION_ID, notificationId)
|
||||||
}
|
}
|
||||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
|
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -258,7 +261,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
putExtra(EXTRA_MANGA_ID, manga.id)
|
putExtra(EXTRA_MANGA_ID, manga.id)
|
||||||
putExtra(EXTRA_CHAPTER_ID, chapter.id)
|
putExtra(EXTRA_CHAPTER_ID, chapter.id)
|
||||||
}
|
}
|
||||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
|
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -271,7 +274,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
||||||
action = ACTION_CANCEL_LIBRARY_UPDATE
|
action = ACTION_CANCEL_LIBRARY_UPDATE
|
||||||
}
|
}
|
||||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
|
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -65,12 +65,18 @@ class PreferenceKeys(context: Context) {
|
|||||||
|
|
||||||
val enabledLanguages = context.getString(R.string.pref_source_languages)
|
val enabledLanguages = context.getString(R.string.pref_source_languages)
|
||||||
|
|
||||||
|
val backupDirectory = context.getString(R.string.pref_backup_directory_key)
|
||||||
|
|
||||||
val downloadsDirectory = context.getString(R.string.pref_download_directory_key)
|
val downloadsDirectory = context.getString(R.string.pref_download_directory_key)
|
||||||
|
|
||||||
val downloadThreads = context.getString(R.string.pref_download_slots_key)
|
val downloadThreads = context.getString(R.string.pref_download_slots_key)
|
||||||
|
|
||||||
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 numberOfBackups = context.getString(R.string.pref_backup_slots_key)
|
||||||
|
|
||||||
|
val backupInterval = context.getString(R.string.pref_backup_interval_key)
|
||||||
|
|
||||||
val removeAfterReadSlots = context.getString(R.string.pref_remove_after_read_slots_key)
|
val removeAfterReadSlots = context.getString(R.string.pref_remove_after_read_slots_key)
|
||||||
|
|
||||||
val removeAfterMarkedAsRead = context.getString(R.string.pref_remove_after_marked_as_read_key)
|
val removeAfterMarkedAsRead = context.getString(R.string.pref_remove_after_marked_as_read_key)
|
||||||
@ -93,6 +99,8 @@ class PreferenceKeys(context: Context) {
|
|||||||
|
|
||||||
val downloadNew = context.getString(R.string.pref_download_new_key)
|
val downloadNew = context.getString(R.string.pref_download_new_key)
|
||||||
|
|
||||||
|
val downloadNewCategories = context.getString(R.string.pref_download_new_categories_key)
|
||||||
|
|
||||||
fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId"
|
fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId"
|
||||||
|
|
||||||
fun sourcePassword(sourceId: Long) = "pref_source_password_$sourceId"
|
fun sourcePassword(sourceId: Long) = "pref_source_password_$sourceId"
|
||||||
@ -107,4 +115,6 @@ class PreferenceKeys(context: Context) {
|
|||||||
|
|
||||||
val lang = context.getString(R.string.pref_language_key)
|
val lang = context.getString(R.string.pref_language_key)
|
||||||
|
|
||||||
|
val defaultCategory = context.getString(R.string.default_category_key)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,10 @@ class PreferencesHelper(val context: Context) {
|
|||||||
File(Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
File(Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
||||||
context.getString(R.string.app_name), "downloads"))
|
context.getString(R.string.app_name), "downloads"))
|
||||||
|
|
||||||
|
private val defaultBackupDir = Uri.fromFile(
|
||||||
|
File(Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
||||||
|
context.getString(R.string.app_name), "backup"))
|
||||||
|
|
||||||
fun startScreen() = prefs.getInt(keys.startScreen, 1)
|
fun startScreen() = prefs.getInt(keys.startScreen, 1)
|
||||||
|
|
||||||
fun clear() = prefs.edit().clear().apply()
|
fun clear() = prefs.edit().clear().apply()
|
||||||
@ -112,12 +116,18 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun anilistScoreType() = rxPrefs.getInteger("anilist_score_type", 0)
|
fun anilistScoreType() = rxPrefs.getInteger("anilist_score_type", 0)
|
||||||
|
|
||||||
|
fun backupsDirectory() = rxPrefs.getString(keys.backupDirectory, defaultBackupDir.toString())
|
||||||
|
|
||||||
fun downloadsDirectory() = rxPrefs.getString(keys.downloadsDirectory, defaultDownloadsDir.toString())
|
fun downloadsDirectory() = rxPrefs.getString(keys.downloadsDirectory, defaultDownloadsDir.toString())
|
||||||
|
|
||||||
fun downloadThreads() = rxPrefs.getInteger(keys.downloadThreads, 1)
|
fun downloadThreads() = rxPrefs.getInteger(keys.downloadThreads, 1)
|
||||||
|
|
||||||
fun downloadOnlyOverWifi() = prefs.getBoolean(keys.downloadOnlyOverWifi, true)
|
fun downloadOnlyOverWifi() = prefs.getBoolean(keys.downloadOnlyOverWifi, true)
|
||||||
|
|
||||||
|
fun numberOfBackups() = rxPrefs.getInteger(keys.numberOfBackups, 1)
|
||||||
|
|
||||||
|
fun backupInterval() = rxPrefs.getInteger(keys.backupInterval, 0)
|
||||||
|
|
||||||
fun removeAfterReadSlots() = prefs.getInt(keys.removeAfterReadSlots, -1)
|
fun removeAfterReadSlots() = prefs.getInt(keys.removeAfterReadSlots, -1)
|
||||||
|
|
||||||
fun removeAfterMarkedAsRead() = prefs.getBoolean(keys.removeAfterMarkedAsRead, false)
|
fun removeAfterMarkedAsRead() = prefs.getBoolean(keys.removeAfterMarkedAsRead, false)
|
||||||
@ -142,8 +152,12 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun hiddenCatalogues() = rxPrefs.getStringSet("hidden_catalogues", emptySet())
|
fun hiddenCatalogues() = rxPrefs.getStringSet("hidden_catalogues", emptySet())
|
||||||
|
|
||||||
fun downloadNew() = prefs.getBoolean(keys.downloadNew, false)
|
fun downloadNew() = rxPrefs.getBoolean(keys.downloadNew, false)
|
||||||
|
|
||||||
|
fun downloadNewCategories() = rxPrefs.getStringSet(keys.downloadNewCategories, emptySet())
|
||||||
|
|
||||||
fun lang() = prefs.getString(keys.lang, "")
|
fun lang() = prefs.getString(keys.lang, "")
|
||||||
|
|
||||||
|
fun defaultCategory() = prefs.getInt(keys.defaultCategory, -1)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.data.track.TrackService
|
|||||||
import rx.Completable
|
import rx.Completable
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.text.DecimalFormat
|
||||||
|
|
||||||
class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
||||||
|
|
||||||
@ -55,11 +56,17 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getScoreList(): List<String> {
|
override fun getScoreList(): List<String> {
|
||||||
return IntRange(0, 10).map { (it.toFloat() / 2).toString() }
|
val df = DecimalFormat("0.#")
|
||||||
|
return listOf("0") + IntRange(2, 20).map { df.format(it / 2f) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun indexToScore(index: Int): Float {
|
||||||
|
return if (index > 0) (index + 1) / 2f else 0f
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun displayScore(track: Track): String {
|
override fun displayScore(track: Track): String {
|
||||||
return track.toKitsuScore()
|
val df = DecimalFormat("0.#")
|
||||||
|
return df.format(track.score)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun add(track: Track): Observable<Track> {
|
override fun add(track: Track): Observable<Track> {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.kitsu
|
package eu.kanade.tachiyomi.data.track.kitsu
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.*
|
import com.github.salomonbrys.kotson.*
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.JsonObject
|
import com.google.gson.JsonObject
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
@ -17,7 +18,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
private val rest = Retrofit.Builder()
|
private val rest = Retrofit.Builder()
|
||||||
.baseUrl(baseUrl)
|
.baseUrl(baseUrl)
|
||||||
.client(client.newBuilder().addInterceptor(interceptor).build())
|
.client(client.newBuilder().addInterceptor(interceptor).build())
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().serializeNulls().create()))
|
||||||
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
||||||
.build()
|
.build()
|
||||||
.create(KitsuApi.Rest::class.java)
|
.create(KitsuApi.Rest::class.java)
|
||||||
@ -65,7 +66,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
"attributes" to jsonObject(
|
"attributes" to jsonObject(
|
||||||
"status" to track.toKitsuStatus(),
|
"status" to track.toKitsuStatus(),
|
||||||
"progress" to track.last_chapter_read,
|
"progress" to track.last_chapter_read,
|
||||||
"rating" to track.toKitsuScore()
|
"ratingTwenty" to track.toKitsuScore()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
|
@ -23,13 +23,13 @@ open class KitsuManga(obj: JsonObject) {
|
|||||||
class KitsuLibManga(obj: JsonObject, manga: JsonObject) : KitsuManga(manga) {
|
class KitsuLibManga(obj: JsonObject, manga: JsonObject) : KitsuManga(manga) {
|
||||||
val remoteId by obj.byInt("id")
|
val remoteId by obj.byInt("id")
|
||||||
val status by obj["attributes"].byString
|
val status by obj["attributes"].byString
|
||||||
val rating = obj["attributes"].obj.get("rating").nullString
|
val ratingTwenty = obj["attributes"].obj.get("ratingTwenty").nullString
|
||||||
val progress by obj["attributes"].byInt
|
val progress by obj["attributes"].byInt
|
||||||
|
|
||||||
override fun toTrack() = super.toTrack().apply {
|
override fun toTrack() = super.toTrack().apply {
|
||||||
remote_id = remoteId
|
remote_id = remoteId
|
||||||
status = toTrackStatus()
|
status = toTrackStatus()
|
||||||
score = rating?.let { it.toFloat() * 2 } ?: 0f
|
score = ratingTwenty?.let { it.toInt() / 2f } ?: 0f
|
||||||
last_chapter_read = progress
|
last_chapter_read = progress
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,6 +53,6 @@ fun Track.toKitsuStatus() = when (status) {
|
|||||||
else -> throw Exception("Unknown status")
|
else -> throw Exception("Unknown status")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Track.toKitsuScore(): String {
|
fun Track.toKitsuScore(): String? {
|
||||||
return if (score > 0) (score / 2).toString() else ""
|
return if (score > 0) (score * 2).toInt().toString() else null
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ import android.content.BroadcastReceiver
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.os.Build
|
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
@ -59,14 +58,21 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
|
|||||||
fun downloadApk(url: String) {
|
fun downloadApk(url: String) {
|
||||||
// Show notification download starting.
|
// Show notification download starting.
|
||||||
sendInitialBroadcast()
|
sendInitialBroadcast()
|
||||||
// Progress of the download
|
|
||||||
var savedProgress = 0
|
|
||||||
|
|
||||||
val progressListener = object : ProgressListener {
|
val progressListener = object : ProgressListener {
|
||||||
|
|
||||||
|
// Progress of the download
|
||||||
|
var savedProgress = 0
|
||||||
|
|
||||||
|
// Keep track of the last notification sent to avoid posting too many.
|
||||||
|
var lastTick = 0L
|
||||||
|
|
||||||
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
|
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
|
||||||
val progress = (100 * bytesRead / contentLength).toInt()
|
val progress = (100 * bytesRead / contentLength).toInt()
|
||||||
if (progress > savedProgress) {
|
val currentTime = System.currentTimeMillis()
|
||||||
|
if (progress > savedProgress && currentTime - 200 > lastTick) {
|
||||||
savedProgress = progress
|
savedProgress = progress
|
||||||
|
lastTick = currentTime
|
||||||
sendProgressBroadcast(progress)
|
sendProgressBroadcast(progress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -112,11 +118,7 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
|
|||||||
putExtra(UpdateDownloaderReceiver.EXTRA_ACTION, UpdateDownloaderReceiver.NOTIFICATION_UPDATER_PROGRESS)
|
putExtra(UpdateDownloaderReceiver.EXTRA_ACTION, UpdateDownloaderReceiver.NOTIFICATION_UPDATER_PROGRESS)
|
||||||
putExtra(UpdateDownloaderReceiver.EXTRA_PROGRESS, progress)
|
putExtra(UpdateDownloaderReceiver.EXTRA_PROGRESS, progress)
|
||||||
}
|
}
|
||||||
// Prevents not showing of install notification TODO weird Android N bug. Find out what goes wrong
|
sendLocalBroadcastSync(intent)
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || progress <= 95) {
|
|
||||||
// Show download progress notification.
|
|
||||||
sendLocalBroadcastSync(intent)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -10,7 +10,7 @@ import rx.Subscription
|
|||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
fun Call.asObservable(): Observable<Response> {
|
fun Call.asObservable(): Observable<Response> {
|
||||||
return Observable.create { subscriber ->
|
return Observable.unsafeCreate { subscriber ->
|
||||||
// Since Call is a one-shot type, clone it for each new subscriber.
|
// Since Call is a one-shot type, clone it for each new subscriber.
|
||||||
val call = clone()
|
val call = clone()
|
||||||
|
|
||||||
|
@ -176,8 +176,7 @@ class YamlHttpSource(mappings: Map<*, *>) : HttpSource() {
|
|||||||
|
|
||||||
val pages = mutableListOf<Page>()
|
val pages = mutableListOf<Page>()
|
||||||
|
|
||||||
// TODO lazy initialization in Kotlin 1.1
|
val document by lazy { Jsoup.parse(body, url) }
|
||||||
val document = Jsoup.parse(body, url)
|
|
||||||
|
|
||||||
with(map.pages) {
|
with(map.pages) {
|
||||||
// Capture a list of values where page urls will be resolved.
|
// Capture a list of values where page urls will be resolved.
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.source.online.english
|
package eu.kanade.tachiyomi.source.online.english
|
||||||
|
|
||||||
|
import com.squareup.duktape.Duktape
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.source.model.*
|
import eu.kanade.tachiyomi.source.model.*
|
||||||
@ -114,15 +115,37 @@ class Kissmanga : ParsedHttpSource() {
|
|||||||
override fun pageListRequest(chapter: SChapter) = POST(baseUrl + chapter.url, headers)
|
override fun pageListRequest(chapter: SChapter) = POST(baseUrl + chapter.url, headers)
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
val pages = mutableListOf<Page>()
|
val body = response.body().string()
|
||||||
//language=RegExp
|
|
||||||
val p = Pattern.compile("""lstImages.push\("(.+?)"""")
|
|
||||||
val m = p.matcher(response.body().string())
|
|
||||||
|
|
||||||
var i = 0
|
val pages = mutableListOf<Page>()
|
||||||
while (m.find()) {
|
|
||||||
pages.add(Page(i++, "", m.group(1)))
|
// Kissmanga now encrypts the urls, so we need to execute these two scripts in JS.
|
||||||
|
val ca = client.newCall(GET("$baseUrl/Scripts/ca.js", headers)).execute().body().string()
|
||||||
|
val lo = client.newCall(GET("$baseUrl/Scripts/lo.js", headers)).execute().body().string()
|
||||||
|
|
||||||
|
Duktape.create().use {
|
||||||
|
it.evaluate(ca)
|
||||||
|
it.evaluate(lo)
|
||||||
|
|
||||||
|
// There are two functions in an inline script needed to decrypt the urls. We find and
|
||||||
|
// execute them.
|
||||||
|
var p = Pattern.compile("(.*CryptoJS.*)")
|
||||||
|
var m = p.matcher(body)
|
||||||
|
while (m.find()) {
|
||||||
|
it.evaluate(m.group(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally find all the urls and decrypt them in JS.
|
||||||
|
p = Pattern.compile("""lstImages.push\((.*)\);""")
|
||||||
|
m = p.matcher(body)
|
||||||
|
|
||||||
|
var i = 0
|
||||||
|
while (m.find()) {
|
||||||
|
val url = it.evaluate(m.group(1)) as String
|
||||||
|
pages.add(Page(i++, "", url))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return pages
|
return pages
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,6 +152,11 @@ class Mangahere : ParsedHttpSource() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
|
val licensedError = document.select(".mangaread_error > .mt10").first()
|
||||||
|
if (licensedError != null) {
|
||||||
|
throw Exception(licensedError.text())
|
||||||
|
}
|
||||||
|
|
||||||
val pages = mutableListOf<Page>()
|
val pages = mutableListOf<Page>()
|
||||||
document.select("select.wid60").first()?.getElementsByTag("option")?.forEach {
|
document.select("select.wid60").first()?.getElementsByTag("option")?.forEach {
|
||||||
pages.add(Page(pages.size, it.attr("value")))
|
pages.add(Page(pages.size, it.attr("value")))
|
||||||
|
@ -4,6 +4,7 @@ import eu.kanade.tachiyomi.network.POST
|
|||||||
import eu.kanade.tachiyomi.source.model.*
|
import eu.kanade.tachiyomi.source.model.*
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
@ -27,11 +28,16 @@ class Mangasee : ParsedHttpSource() {
|
|||||||
|
|
||||||
private val indexPattern = Pattern.compile("-index-(.*?)-")
|
private val indexPattern = Pattern.compile("-index-(.*?)-")
|
||||||
|
|
||||||
|
private val catalogHeaders = Headers.Builder().apply {
|
||||||
|
add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)")
|
||||||
|
add("Host", "mangaseeonline.us")
|
||||||
|
}.build()
|
||||||
|
|
||||||
override fun popularMangaSelector() = "div.requested > div.row"
|
override fun popularMangaSelector() = "div.requested > div.row"
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
val (body, requestUrl) = convertQueryToPost(page, "$baseUrl/search/request.php?sortBy=popularity&sortOrder=descending")
|
val (body, requestUrl) = convertQueryToPost(page, "$baseUrl/search/request.php?sortBy=popularity&sortOrder=descending")
|
||||||
return POST(requestUrl, headers, body.build())
|
return POST(requestUrl, catalogHeaders, body.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element): SManga {
|
override fun popularMangaFromElement(element: Element): SManga {
|
||||||
@ -74,7 +80,7 @@ class Mangasee : ParsedHttpSource() {
|
|||||||
if (genresNo.isNotEmpty()) url.addQueryParameter("genreNo", genresNo.joinToString(","))
|
if (genresNo.isNotEmpty()) url.addQueryParameter("genreNo", genresNo.joinToString(","))
|
||||||
|
|
||||||
val (body, requestUrl) = convertQueryToPost(page, url.toString())
|
val (body, requestUrl) = convertQueryToPost(page, url.toString())
|
||||||
return POST(requestUrl, headers, body.build())
|
return POST(requestUrl, catalogHeaders, body.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun convertQueryToPost(page: Int, url: String): Pair<FormBody.Builder, String> {
|
private fun convertQueryToPost(page: Int, url: String): Pair<FormBody.Builder, String> {
|
||||||
@ -164,7 +170,7 @@ class Mangasee : ParsedHttpSource() {
|
|||||||
override fun latestUpdatesRequest(page: Int): Request {
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
val url = "http://mangaseeonline.net/home/latest.request.php"
|
val url = "http://mangaseeonline.net/home/latest.request.php"
|
||||||
val (body, requestUrl) = convertQueryToPost(page, url)
|
val (body, requestUrl) = convertQueryToPost(page, url)
|
||||||
return POST(requestUrl, headers, body.build())
|
return POST(requestUrl, catalogHeaders, body.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||||
|
@ -1,163 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.backup
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.ui.base.activity.ActivityMixin
|
|
||||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
|
|
||||||
import eu.kanade.tachiyomi.util.toast
|
|
||||||
import kotlinx.android.synthetic.main.fragment_backup.*
|
|
||||||
import nucleus.factory.RequiresPresenter
|
|
||||||
import rx.Observable
|
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
|
||||||
import rx.internal.util.SubscriptionList
|
|
||||||
import rx.schedulers.Schedulers
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.io.File
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fragment to create and restore backups of the application's data.
|
|
||||||
* Uses R.layout.fragment_backup.
|
|
||||||
*/
|
|
||||||
@RequiresPresenter(BackupPresenter::class)
|
|
||||||
class BackupFragment : BaseRxFragment<BackupPresenter>() {
|
|
||||||
|
|
||||||
private var backupDialog: Dialog? = null
|
|
||||||
private var restoreDialog: Dialog? = null
|
|
||||||
|
|
||||||
private lateinit var subscriptions: SubscriptionList
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View {
|
|
||||||
return inflater.inflate(R.layout.fragment_backup, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedState: Bundle?) {
|
|
||||||
setToolbarTitle(getString(R.string.label_backup))
|
|
||||||
|
|
||||||
(activity as ActivityMixin).requestPermissionsOnMarshmallow()
|
|
||||||
subscriptions = SubscriptionList()
|
|
||||||
|
|
||||||
backup_button.setOnClickListener {
|
|
||||||
val today = SimpleDateFormat("yyyy-MM-dd").format(Date())
|
|
||||||
val file = File(activity.externalCacheDir, "tachiyomi-$today.json")
|
|
||||||
presenter.createBackup(file)
|
|
||||||
|
|
||||||
backupDialog = MaterialDialog.Builder(activity)
|
|
||||||
.content(R.string.backup_please_wait)
|
|
||||||
.progress(true, 0)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
restore_button.setOnClickListener {
|
|
||||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
intent.type = "application/*"
|
|
||||||
val chooser = Intent.createChooser(intent, getString(R.string.file_select_backup))
|
|
||||||
startActivityForResult(chooser, REQUEST_BACKUP_OPEN)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
subscriptions.unsubscribe()
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called from the presenter when the backup is completed.
|
|
||||||
*
|
|
||||||
* @param file the file where the backup is saved.
|
|
||||||
*/
|
|
||||||
fun onBackupCompleted(file: File) {
|
|
||||||
dismissBackupDialog()
|
|
||||||
val intent = Intent(Intent.ACTION_SEND)
|
|
||||||
intent.type = "application/json"
|
|
||||||
intent.putExtra(Intent.EXTRA_STREAM, Uri.parse("file://" + file))
|
|
||||||
startActivity(Intent.createChooser(intent, ""))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called from the presenter when the restore is completed.
|
|
||||||
*/
|
|
||||||
fun onRestoreCompleted() {
|
|
||||||
dismissRestoreDialog()
|
|
||||||
context.toast(R.string.backup_completed)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called from the presenter when there's an error doing the backup.
|
|
||||||
* @param error the exception thrown.
|
|
||||||
*/
|
|
||||||
fun onBackupError(error: Throwable) {
|
|
||||||
dismissBackupDialog()
|
|
||||||
context.toast(error.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called from the presenter when there's an error restoring the backup.
|
|
||||||
* @param error the exception thrown.
|
|
||||||
*/
|
|
||||||
fun onRestoreError(error: Throwable) {
|
|
||||||
dismissRestoreDialog()
|
|
||||||
context.toast(error.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
if (data != null && resultCode == Activity.RESULT_OK && requestCode == REQUEST_BACKUP_OPEN) {
|
|
||||||
restoreDialog = MaterialDialog.Builder(activity)
|
|
||||||
.content(R.string.restore_please_wait)
|
|
||||||
.progress(true, 0)
|
|
||||||
.show()
|
|
||||||
|
|
||||||
// When using cloud services, we have to open the input stream in a background thread.
|
|
||||||
Observable.fromCallable { context.contentResolver.openInputStream(data.data) }
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe({
|
|
||||||
presenter.restoreBackup(it)
|
|
||||||
}, { error ->
|
|
||||||
context.toast(error.message)
|
|
||||||
Timber.e(error)
|
|
||||||
})
|
|
||||||
.apply { subscriptions.add(this) }
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dismisses the backup dialog.
|
|
||||||
*/
|
|
||||||
fun dismissBackupDialog() {
|
|
||||||
backupDialog?.let {
|
|
||||||
it.dismiss()
|
|
||||||
backupDialog = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dismisses the restore dialog.
|
|
||||||
*/
|
|
||||||
fun dismissRestoreDialog() {
|
|
||||||
restoreDialog?.let {
|
|
||||||
it.dismiss()
|
|
||||||
restoreDialog = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private val REQUEST_BACKUP_OPEN = 102
|
|
||||||
|
|
||||||
fun newInstance(): BackupFragment {
|
|
||||||
return BackupFragment()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,94 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.backup
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupManager
|
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
|
||||||
import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
|
|
||||||
import rx.Observable
|
|
||||||
import rx.Subscription
|
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
|
||||||
import rx.schedulers.Schedulers
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.io.File
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Presenter of [BackupFragment].
|
|
||||||
*/
|
|
||||||
class BackupPresenter : BasePresenter<BackupFragment>() {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Database.
|
|
||||||
*/
|
|
||||||
val db: DatabaseHelper by injectLazy()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Backup manager.
|
|
||||||
*/
|
|
||||||
private lateinit var backupManager: BackupManager
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscription where the backup is restored.
|
|
||||||
*/
|
|
||||||
private var restoreSubscription: Subscription? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscription where the backup is created.
|
|
||||||
*/
|
|
||||||
private var backupSubscription: Subscription? = null
|
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
|
||||||
super.onCreate(savedState)
|
|
||||||
backupManager = BackupManager(db)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a backup and saves it to a file.
|
|
||||||
*
|
|
||||||
* @param file the path where the file will be saved.
|
|
||||||
*/
|
|
||||||
fun createBackup(file: File) {
|
|
||||||
if (backupSubscription.isNullOrUnsubscribed()) {
|
|
||||||
backupSubscription = getBackupObservable(file)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribeFirst(
|
|
||||||
{ view, result -> view.onBackupCompleted(file) },
|
|
||||||
BackupFragment::onBackupError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restores a backup from a stream.
|
|
||||||
*
|
|
||||||
* @param stream the input stream of the backup file.
|
|
||||||
*/
|
|
||||||
fun restoreBackup(stream: InputStream) {
|
|
||||||
if (restoreSubscription.isNullOrUnsubscribed()) {
|
|
||||||
restoreSubscription = getRestoreObservable(stream)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribeFirst(
|
|
||||||
{ view, result -> view.onRestoreCompleted() },
|
|
||||||
BackupFragment::onRestoreError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the observable to save a backup.
|
|
||||||
*/
|
|
||||||
private fun getBackupObservable(file: File) = Observable.fromCallable {
|
|
||||||
backupManager.backupToFile(file)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the observable to restore a backup.
|
|
||||||
*/
|
|
||||||
private fun getRestoreObservable(stream: InputStream) = Observable.fromCallable {
|
|
||||||
backupManager.restoreFromStream(stream)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,6 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.ui.base.fragment
|
package eu.kanade.tachiyomi.ui.base.fragment
|
||||||
|
|
||||||
import android.support.v7.app.AppCompatActivity
|
import android.support.v4.app.FragmentActivity
|
||||||
import eu.kanade.tachiyomi.ui.base.activity.ActivityMixin
|
import eu.kanade.tachiyomi.ui.base.activity.ActivityMixin
|
||||||
|
|
||||||
interface FragmentMixin {
|
interface FragmentMixin {
|
||||||
@ -13,7 +13,7 @@ interface FragmentMixin {
|
|||||||
(getActivity() as ActivityMixin).setToolbarTitle(getString(resourceId))
|
(getActivity() as ActivityMixin).setToolbarTitle(getString(resourceId))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getActivity(): AppCompatActivity
|
fun getActivity(): FragmentActivity
|
||||||
|
|
||||||
fun getString(resource: Int): String
|
fun getString(resource: Int): String
|
||||||
}
|
}
|
@ -4,6 +4,7 @@ import android.content.res.Configuration
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.design.widget.Snackbar
|
import android.support.design.widget.Snackbar
|
||||||
import android.support.v4.widget.DrawerLayout
|
import android.support.v4.widget.DrawerLayout
|
||||||
|
import android.support.v7.app.AppCompatActivity
|
||||||
import android.support.v7.widget.*
|
import android.support.v7.widget.*
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
@ -14,7 +15,9 @@ import com.f2prateek.rx.preferences.Preference
|
|||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.davidea.flexibleadapter.items.IFlexible
|
import eu.davidea.flexibleadapter.items.IFlexible
|
||||||
import eu.kanade.tachiyomi.R
|
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.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
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
|
||||||
@ -32,6 +35,7 @@ import nucleus.factory.RequiresPresenter
|
|||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.subjects.PublishSubject
|
import rx.subjects.PublishSubject
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.util.concurrent.TimeUnit.MILLISECONDS
|
import java.util.concurrent.TimeUnit.MILLISECONDS
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -44,6 +48,11 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
|
|||||||
FlexibleAdapter.OnItemLongClickListener,
|
FlexibleAdapter.OnItemLongClickListener,
|
||||||
FlexibleAdapter.EndlessScrollListener<ProgressItem> {
|
FlexibleAdapter.EndlessScrollListener<ProgressItem> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preferences helper.
|
||||||
|
*/
|
||||||
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Spinner shown in the toolbar to change the selected source.
|
* Spinner shown in the toolbar to change the selected source.
|
||||||
*/
|
*/
|
||||||
@ -155,7 +164,7 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
|
|||||||
setupRecycler()
|
setupRecycler()
|
||||||
|
|
||||||
// Create toolbar spinner
|
// Create toolbar spinner
|
||||||
val themedContext = activity.supportActionBar?.themedContext ?: activity
|
val themedContext = (activity as AppCompatActivity).supportActionBar?.themedContext ?: activity
|
||||||
|
|
||||||
val spinnerAdapter = ArrayAdapter(themedContext,
|
val spinnerAdapter = ArrayAdapter(themedContext,
|
||||||
android.R.layout.simple_spinner_item, presenter.sources)
|
android.R.layout.simple_spinner_item, presenter.sources)
|
||||||
@ -529,23 +538,72 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
|
|||||||
/**
|
/**
|
||||||
* Called when a manga is long clicked.
|
* Called when a manga is long clicked.
|
||||||
*
|
*
|
||||||
|
* Adds the manga to the default category if none is set it shows a list of categories for the user to put the manga
|
||||||
|
* in, the list consists of the default category plus the user's categories. The default category is preselected on
|
||||||
|
* new manga, and on already favorited manga the manga's categories are preselected.
|
||||||
|
*
|
||||||
* @param position the position of the element clicked.
|
* @param position the position of the element clicked.
|
||||||
*/
|
*/
|
||||||
override fun onItemLongClick(position: Int) {
|
override fun onItemLongClick(position: Int) {
|
||||||
|
// Get manga
|
||||||
val manga = (adapter.getItem(position) as? CatalogueItem?)?.manga ?: return
|
val manga = (adapter.getItem(position) as? CatalogueItem?)?.manga ?: return
|
||||||
|
// Fetch categories
|
||||||
|
val categories = presenter.getCategories()
|
||||||
|
|
||||||
val textRes = if (manga.favorite) R.string.remove_from_library else R.string.add_to_library
|
if (manga.favorite){
|
||||||
|
MaterialDialog.Builder(activity)
|
||||||
MaterialDialog.Builder(activity)
|
.items(getString(R.string.remove_from_library ))
|
||||||
.items(getString(textRes))
|
.itemsCallback { _, _, which, _ ->
|
||||||
.itemsCallback { dialog, itemView, which, text ->
|
when (which) {
|
||||||
when (which) {
|
0 -> {
|
||||||
0 -> {
|
presenter.changeMangaFavorite(manga)
|
||||||
presenter.changeMangaFavorite(manga)
|
adapter.notifyItemChanged(position)
|
||||||
adapter.notifyItemChanged(position)
|
}
|
||||||
}
|
}
|
||||||
}
|
}.show()
|
||||||
}.show()
|
}else{
|
||||||
|
val defaultCategory = categories.find { it.id == preferences.defaultCategory()}
|
||||||
|
if(defaultCategory != null) {
|
||||||
|
presenter.changeMangaFavorite(manga)
|
||||||
|
presenter.moveMangaToCategory(defaultCategory, manga)
|
||||||
|
// Show manga has been added
|
||||||
|
context.toast(R.string.added_to_library)
|
||||||
|
adapter.notifyItemChanged(position)
|
||||||
|
} else {
|
||||||
|
MaterialDialog.Builder(activity)
|
||||||
|
.title(R.string.action_move_category)
|
||||||
|
.items(categories.map { it.name })
|
||||||
|
.itemsCallbackMultiChoice(presenter.getMangaCategoryIds(manga)) { dialog, position, _ ->
|
||||||
|
if (position.contains(0) && position.count() > 1) {
|
||||||
|
// Deselect default category
|
||||||
|
dialog.setSelectedIndices(position.filter {it > 0}.toTypedArray())
|
||||||
|
dialog.context.toast(R.string.invalid_combination)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
.alwaysCallMultiChoiceCallback()
|
||||||
|
.positiveText(android.R.string.ok)
|
||||||
|
.negativeText(android.R.string.cancel)
|
||||||
|
.onPositive { dialog, _ ->
|
||||||
|
val selectedCategories = dialog.selectedIndices?.map { categories[it] } ?: emptyList()
|
||||||
|
updateMangaCategories(manga, selectedCategories, position)
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update manga to use selected categories.
|
||||||
|
*
|
||||||
|
* @param manga needed to change
|
||||||
|
* @param selectedCategories selected categories
|
||||||
|
* @param position position of adapter
|
||||||
|
*/
|
||||||
|
private fun updateMangaCategories(manga: Manga, selectedCategories: List<Category>, position: Int) {
|
||||||
|
presenter.updateMangaCategories(manga,selectedCategories)
|
||||||
|
adapter.notifyItemChanged(position)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,9 @@ import eu.davidea.flexibleadapter.items.IFlexible
|
|||||||
import eu.davidea.flexibleadapter.items.ISectionable
|
import eu.davidea.flexibleadapter.items.ISectionable
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
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.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||||
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.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
@ -396,4 +398,66 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the default, and user categories.
|
||||||
|
*
|
||||||
|
* @return List of categories, default plus user categories
|
||||||
|
*/
|
||||||
|
fun getCategories(): List<Category> {
|
||||||
|
return arrayListOf(Category.createDefault()) + db.getCategories().executeAsBlocking()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the category id's the manga is in, if the manga is not in a category, returns the default id.
|
||||||
|
*
|
||||||
|
* @param manga the manga to get categories from.
|
||||||
|
* @return Array of category ids the manga is in, if none returns default id
|
||||||
|
*/
|
||||||
|
fun getMangaCategoryIds(manga: Manga): Array<Int?> {
|
||||||
|
val categories = db.getCategoriesForManga(manga).executeAsBlocking()
|
||||||
|
if (categories.isEmpty()) {
|
||||||
|
return arrayListOf(Category.createDefault().id).toTypedArray()
|
||||||
|
}
|
||||||
|
return categories.map { it.id }.toTypedArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the given manga to categories.
|
||||||
|
*
|
||||||
|
* @param categories the selected categories.
|
||||||
|
* @param manga the manga to move.
|
||||||
|
*/
|
||||||
|
fun moveMangaToCategories(categories: List<Category>, manga: Manga) {
|
||||||
|
val mc = categories.map { MangaCategory.create(manga, it) }
|
||||||
|
|
||||||
|
db.setMangaCategories(mc, arrayListOf(manga))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the given manga to the category.
|
||||||
|
*
|
||||||
|
* @param category the selected category.
|
||||||
|
* @param manga the manga to move.
|
||||||
|
*/
|
||||||
|
fun moveMangaToCategory(category: Category, manga: Manga) {
|
||||||
|
moveMangaToCategories(arrayListOf(category), manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update manga to use selected categories.
|
||||||
|
*
|
||||||
|
* @param manga needed to change
|
||||||
|
* @param selectedCategories selected categories
|
||||||
|
*/
|
||||||
|
fun updateMangaCategories(manga: Manga, selectedCategories: List<Category>) {
|
||||||
|
if (!selectedCategories.isEmpty()) {
|
||||||
|
if (!manga.favorite)
|
||||||
|
changeMangaFavorite(manga)
|
||||||
|
|
||||||
|
moveMangaToCategories(selectedCategories.filter { it.id != 0 }, manga)
|
||||||
|
} else {
|
||||||
|
changeMangaFavorite(manga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import android.support.design.widget.TabLayout
|
|||||||
import android.support.v4.graphics.drawable.DrawableCompat
|
import android.support.v4.graphics.drawable.DrawableCompat
|
||||||
import android.support.v4.view.ViewPager
|
import android.support.v4.view.ViewPager
|
||||||
import android.support.v4.widget.DrawerLayout
|
import android.support.v4.widget.DrawerLayout
|
||||||
|
import android.support.v7.app.AppCompatActivity
|
||||||
import android.support.v7.view.ActionMode
|
import android.support.v7.view.ActionMode
|
||||||
import android.support.v7.widget.SearchView
|
import android.support.v7.widget.SearchView
|
||||||
import android.view.*
|
import android.view.*
|
||||||
@ -356,7 +357,7 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
|||||||
*/
|
*/
|
||||||
fun createActionModeIfNeeded() {
|
fun createActionModeIfNeeded() {
|
||||||
if (actionMode == null) {
|
if (actionMode == null) {
|
||||||
actionMode = activity.startSupportActionMode(this)
|
actionMode = (activity as AppCompatActivity).startSupportActionMode(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -440,11 +441,11 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
|||||||
if (presenter.editCoverWithStream(it, manga)) {
|
if (presenter.editCoverWithStream(it, manga)) {
|
||||||
// TODO refresh cover
|
// TODO refresh cover
|
||||||
} else {
|
} else {
|
||||||
context.toast(R.string.notification_manga_update_failed)
|
context.toast(R.string.notification_cover_update_failed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error: IOException) {
|
} catch (error: IOException) {
|
||||||
context.toast(R.string.notification_manga_update_failed)
|
context.toast(R.string.notification_cover_update_failed)
|
||||||
Timber.e(error)
|
Timber.e(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -177,12 +177,9 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
|
|||||||
private fun applySort(map: Map<Int, List<Manga>>): Map<Int, List<Manga>> {
|
private fun applySort(map: Map<Int, List<Manga>>): Map<Int, List<Manga>> {
|
||||||
val sortingMode = preferences.librarySortingMode().getOrDefault()
|
val sortingMode = preferences.librarySortingMode().getOrDefault()
|
||||||
|
|
||||||
// TODO lazy initialization in kotlin 1.1
|
val lastReadManga by lazy {
|
||||||
var lastReadManga: Map<Long, Int>? = null
|
|
||||||
if (sortingMode == LibrarySort.LAST_READ) {
|
|
||||||
var counter = 0
|
var counter = 0
|
||||||
lastReadManga = db.getLastReadManga().executeAsBlocking()
|
db.getLastReadManga().executeAsBlocking().associate { it.id!! to counter++ }
|
||||||
.associate { it.id!! to counter++ }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val sortFn: (Manga, Manga) -> Int = { manga1, manga2 ->
|
val sortFn: (Manga, Manga) -> Int = { manga1, manga2 ->
|
||||||
@ -190,8 +187,8 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
|
|||||||
LibrarySort.ALPHA -> manga1.title.compareTo(manga2.title)
|
LibrarySort.ALPHA -> manga1.title.compareTo(manga2.title)
|
||||||
LibrarySort.LAST_READ -> {
|
LibrarySort.LAST_READ -> {
|
||||||
// Get index of manga, set equal to list if size unknown.
|
// Get index of manga, set equal to list if size unknown.
|
||||||
val manga1LastRead = lastReadManga!![manga1.id!!] ?: lastReadManga!!.size
|
val manga1LastRead = lastReadManga[manga1.id!!] ?: lastReadManga.size
|
||||||
val manga2LastRead = lastReadManga!![manga2.id!!] ?: lastReadManga!!.size
|
val manga2LastRead = lastReadManga[manga2.id!!] ?: lastReadManga.size
|
||||||
manga1LastRead.compareTo(manga2LastRead)
|
manga1LastRead.compareTo(manga2LastRead)
|
||||||
}
|
}
|
||||||
LibrarySort.LAST_UPDATED -> manga2.last_update.compareTo(manga1.last_update)
|
LibrarySort.LAST_UPDATED -> manga2.last_update.compareTo(manga1.last_update)
|
||||||
|
@ -8,7 +8,6 @@ import android.support.v4.view.GravityCompat
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.ui.backup.BackupFragment
|
|
||||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||||
import eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment
|
import eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment
|
||||||
import eu.kanade.tachiyomi.ui.download.DownloadActivity
|
import eu.kanade.tachiyomi.ui.download.DownloadActivity
|
||||||
@ -38,8 +37,8 @@ class MainActivity : BaseActivity() {
|
|||||||
setAppTheme()
|
setAppTheme()
|
||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
|
|
||||||
// Do not let the launcher create a new activity
|
// Do not let the launcher create a new activity http://stackoverflow.com/questions/16283079
|
||||||
if (intent.flags and Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT != 0) {
|
if (!isTaskRoot) {
|
||||||
finish()
|
finish()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -71,7 +70,6 @@ class MainActivity : BaseActivity() {
|
|||||||
val intent = Intent(this, SettingsActivity::class.java)
|
val intent = Intent(this, SettingsActivity::class.java)
|
||||||
startActivityForResult(intent, REQUEST_OPEN_SETTINGS)
|
startActivityForResult(intent, REQUEST_OPEN_SETTINGS)
|
||||||
}
|
}
|
||||||
R.id.nav_drawer_backup -> setFragment(BackupFragment.newInstance(), id)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
drawer.closeDrawer(GravityCompat.START)
|
drawer.closeDrawer(GravityCompat.START)
|
||||||
@ -80,11 +78,19 @@ class MainActivity : BaseActivity() {
|
|||||||
|
|
||||||
if (savedState == null) {
|
if (savedState == null) {
|
||||||
// Set start screen
|
// Set start screen
|
||||||
setSelectedDrawerItem(startScreenId)
|
when (intent.action) {
|
||||||
|
SHORTCUT_LIBRARY -> setSelectedDrawerItem(R.id.nav_drawer_library)
|
||||||
|
SHORTCUT_RECENTLY_UPDATED -> setSelectedDrawerItem(R.id.nav_drawer_recent_updates)
|
||||||
|
SHORTCUT_RECENTLY_READ -> setSelectedDrawerItem(R.id.nav_drawer_recently_read)
|
||||||
|
SHORTCUT_CATALOGUES -> setSelectedDrawerItem(R.id.nav_drawer_catalogues)
|
||||||
|
else -> setSelectedDrawerItem(startScreenId)
|
||||||
|
}
|
||||||
|
|
||||||
// Show changelog if needed
|
// Show changelog if needed
|
||||||
ChangelogDialogFragment.show(this, preferences, supportFragmentManager)
|
ChangelogDialogFragment.show(this, preferences, supportFragmentManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
@ -145,5 +151,10 @@ class MainActivity : BaseActivity() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val REQUEST_OPEN_SETTINGS = 200
|
private const val REQUEST_OPEN_SETTINGS = 200
|
||||||
|
// Shortcut actions
|
||||||
|
private const val SHORTCUT_LIBRARY = "eu.kanade.tachiyomi.SHOW_LIBRARY"
|
||||||
|
private const val SHORTCUT_RECENTLY_UPDATED = "eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
|
||||||
|
private const val SHORTCUT_RECENTLY_READ = "eu.kanade.tachiyomi.SHOW_RECENTLY_READ"
|
||||||
|
private const val SHORTCUT_CATALOGUES = "eu.kanade.tachiyomi.SHOW_CATALOGUES"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,10 @@ package eu.kanade.tachiyomi.ui.manga.chapter
|
|||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.PopupMenu
|
import android.widget.PopupMenu
|
||||||
|
import eu.davidea.viewholders.FlexibleViewHolder
|
||||||
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.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
|
|
||||||
import eu.kanade.tachiyomi.util.getResourceColor
|
import eu.kanade.tachiyomi.util.getResourceColor
|
||||||
import kotlinx.android.synthetic.main.item_chapter.view.*
|
import kotlinx.android.synthetic.main.item_chapter.view.*
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
@ -13,11 +13,10 @@ import java.text.DecimalFormat
|
|||||||
import java.text.DecimalFormatSymbols
|
import java.text.DecimalFormatSymbols
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class ChaptersHolder(
|
class ChapterHolder(
|
||||||
private val view: View,
|
private val view: View,
|
||||||
private val adapter: ChaptersAdapter,
|
private val adapter: ChaptersAdapter)
|
||||||
listener: FlexibleViewHolder.OnListItemClickListener)
|
: FlexibleViewHolder(view, adapter) {
|
||||||
: FlexibleViewHolder(view, adapter, listener) {
|
|
||||||
|
|
||||||
private val readColor = view.context.getResourceColor(android.R.attr.textColorHint)
|
private val readColor = view.context.getResourceColor(android.R.attr.textColorHint)
|
||||||
private val unreadColor = view.context.getResourceColor(android.R.attr.textColorPrimary)
|
private val unreadColor = view.context.getResourceColor(android.R.attr.textColorPrimary)
|
||||||
@ -25,8 +24,6 @@ class ChaptersHolder(
|
|||||||
private val decimalFormat = DecimalFormat("#.###", DecimalFormatSymbols().apply { decimalSeparator = '.' })
|
private val decimalFormat = DecimalFormat("#.###", DecimalFormatSymbols().apply { decimalSeparator = '.' })
|
||||||
private val df = DateFormat.getDateInstance(DateFormat.SHORT)
|
private val df = DateFormat.getDateInstance(DateFormat.SHORT)
|
||||||
|
|
||||||
private var item: ChapterModel? = null
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// We need to post a Runnable to show the popup to make sure that the PopupMenu is
|
// We need to post a Runnable to show the popup to make sure that the PopupMenu is
|
||||||
// correctly positioned. The reason being that the view may change position before the
|
// correctly positioned. The reason being that the view may change position before the
|
||||||
@ -34,10 +31,10 @@ class ChaptersHolder(
|
|||||||
view.chapter_menu.setOnClickListener { it.post { showPopupMenu(it) } }
|
view.chapter_menu.setOnClickListener { it.post { showPopupMenu(it) } }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSetValues(chapter: ChapterModel, manga: Manga?) = with(view) {
|
fun bind(item: ChapterItem, manga: Manga) = with(view) {
|
||||||
item = chapter
|
val chapter = item.chapter
|
||||||
|
|
||||||
chapter_title.text = when (manga?.displayMode) {
|
chapter_title.text = when (manga.displayMode) {
|
||||||
Manga.DISPLAY_NUMBER -> {
|
Manga.DISPLAY_NUMBER -> {
|
||||||
val formattedNumber = decimalFormat.format(chapter.chapter_number.toDouble())
|
val formattedNumber = decimalFormat.format(chapter.chapter_number.toDouble())
|
||||||
context.getString(R.string.display_mode_chapter, formattedNumber)
|
context.getString(R.string.display_mode_chapter, formattedNumber)
|
||||||
@ -62,7 +59,7 @@ class ChaptersHolder(
|
|||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyStatus(chapter.status)
|
notifyStatus(item.status)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun notifyStatus(status: Int) = with(view.download_text) {
|
fun notifyStatus(status: Int) = with(view.download_text) {
|
||||||
@ -75,15 +72,19 @@ class ChaptersHolder(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showPopupMenu(view: View) = item?.let { chapter ->
|
private fun showPopupMenu(view: View) {
|
||||||
|
val item = adapter.getItem(adapterPosition) ?: return
|
||||||
|
|
||||||
// Create a PopupMenu, giving it the clicked view for an anchor
|
// Create a PopupMenu, giving it the clicked view for an anchor
|
||||||
val popup = PopupMenu(view.context, view)
|
val popup = PopupMenu(view.context, view)
|
||||||
|
|
||||||
// Inflate our menu resource into the PopupMenu's Menu
|
// Inflate our menu resource into the PopupMenu's Menu
|
||||||
popup.menuInflater.inflate(R.menu.chapter_single, popup.menu)
|
popup.menuInflater.inflate(R.menu.chapter_single, popup.menu)
|
||||||
|
|
||||||
|
val chapter = item.chapter
|
||||||
|
|
||||||
// Hide download and show delete if the chapter is downloaded
|
// Hide download and show delete if the chapter is downloaded
|
||||||
if (chapter.isDownloaded) {
|
if (item.isDownloaded) {
|
||||||
popup.menu.findItem(R.id.action_download).isVisible = false
|
popup.menu.findItem(R.id.action_download).isVisible = false
|
||||||
popup.menu.findItem(R.id.action_delete).isVisible = true
|
popup.menu.findItem(R.id.action_delete).isVisible = true
|
||||||
}
|
}
|
||||||
@ -104,20 +105,7 @@ class ChaptersHolder(
|
|||||||
|
|
||||||
// Set a listener so we are notified if a menu item is clicked
|
// Set a listener so we are notified if a menu item is clicked
|
||||||
popup.setOnMenuItemClickListener { menuItem ->
|
popup.setOnMenuItemClickListener { menuItem ->
|
||||||
val chapterList = listOf(chapter)
|
adapter.menuItemListener(adapterPosition, menuItem)
|
||||||
|
|
||||||
with(adapter.fragment) {
|
|
||||||
when (menuItem.itemId) {
|
|
||||||
R.id.action_download -> downloadChapters(chapterList)
|
|
||||||
R.id.action_bookmark -> bookmarkChapters(chapterList, true)
|
|
||||||
R.id.action_remove_bookmark -> bookmarkChapters(chapterList, false)
|
|
||||||
R.id.action_delete -> deleteChapters(chapterList)
|
|
||||||
R.id.action_mark_as_read -> markAsRead(chapterList)
|
|
||||||
R.id.action_mark_as_unread -> markAsUnread(chapterList)
|
|
||||||
R.id.action_mark_previous_as_read -> markPreviousAsRead(chapter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
|
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
|
|
||||||
|
class ChapterItem(val chapter: Chapter, val manga: Manga) : AbstractFlexibleItem<ChapterHolder>(),
|
||||||
|
Chapter by chapter {
|
||||||
|
|
||||||
|
private var _status: Int = 0
|
||||||
|
|
||||||
|
var status: Int
|
||||||
|
get() = download?.status ?: _status
|
||||||
|
set(value) { _status = value }
|
||||||
|
|
||||||
|
@Transient var download: Download? = null
|
||||||
|
|
||||||
|
val isDownloaded: Boolean
|
||||||
|
get() = status == Download.DOWNLOADED
|
||||||
|
|
||||||
|
override fun getLayoutRes(): Int {
|
||||||
|
return R.layout.item_chapter
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): ChapterHolder {
|
||||||
|
return ChapterHolder(inflater.inflate(layoutRes, parent, false), adapter as ChaptersAdapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: ChapterHolder, position: Int, payloads: List<Any?>?) {
|
||||||
|
holder.bind(this, manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other is ChapterItem) {
|
||||||
|
return chapter.id!! == other.chapter.id!!
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return chapter.id!!.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,19 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
|
||||||
|
|
||||||
class ChapterModel(c: Chapter) : Chapter by c {
|
|
||||||
|
|
||||||
private var _status: Int = 0
|
|
||||||
|
|
||||||
var status: Int
|
|
||||||
get() = download?.status ?: _status
|
|
||||||
set(value) { _status = value }
|
|
||||||
|
|
||||||
@Transient var download: Download? = null
|
|
||||||
|
|
||||||
val isDownloaded: Boolean
|
|
||||||
get() = status == Download.DOWNLOADED
|
|
||||||
|
|
||||||
}
|
|
@ -1,42 +1,19 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||||
|
|
||||||
import android.view.ViewGroup
|
import android.view.MenuItem
|
||||||
import eu.davidea.flexibleadapter4.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.util.inflate
|
|
||||||
|
|
||||||
class ChaptersAdapter(val fragment: ChaptersFragment) : FlexibleAdapter<ChaptersHolder, ChapterModel>() {
|
class ChaptersAdapter(val fragment: ChaptersFragment) : FlexibleAdapter<ChapterItem>(null, fragment, true) {
|
||||||
|
|
||||||
init {
|
var items: List<ChapterItem> = emptyList()
|
||||||
setHasStableIds(true)
|
|
||||||
|
val menuItemListener: (Int, MenuItem) -> Unit = { position, item ->
|
||||||
|
fragment.onItemMenuClick(position, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
var items: List<ChapterModel>
|
override fun updateDataSet(items: List<ChapterItem>) {
|
||||||
get() = mItems
|
this.items = items
|
||||||
set(value) {
|
super.updateDataSet(items.toList())
|
||||||
mItems = value
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun updateDataSet(param: String) {
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChaptersHolder {
|
|
||||||
val v = parent.inflate(R.layout.item_chapter)
|
|
||||||
return ChaptersHolder(v, this, fragment)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ChaptersHolder, position: Int) {
|
|
||||||
val chapter = getItem(position)
|
|
||||||
val manga = fragment.presenter.manga
|
|
||||||
holder.onSetValues(chapter, manga)
|
|
||||||
|
|
||||||
//When user scrolls this bind the correct selection status
|
|
||||||
holder.itemView.isActivated = isSelected(position)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemId(position: Int): Long {
|
|
||||||
return mItems[position].id!!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -6,17 +6,17 @@ import android.content.Intent
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.design.widget.Snackbar
|
import android.support.design.widget.Snackbar
|
||||||
import android.support.v4.app.DialogFragment
|
import android.support.v4.app.DialogFragment
|
||||||
|
import android.support.v7.app.AppCompatActivity
|
||||||
import android.support.v7.view.ActionMode
|
import android.support.v7.view.ActionMode
|
||||||
import android.support.v7.widget.DividerItemDecoration
|
import android.support.v7.widget.DividerItemDecoration
|
||||||
import android.support.v7.widget.LinearLayoutManager
|
import android.support.v7.widget.LinearLayoutManager
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import eu.davidea.flexibleadapter4.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
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.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
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.manga.MangaActivity
|
import eu.kanade.tachiyomi.ui.manga.MangaActivity
|
||||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||||
@ -29,7 +29,10 @@ import nucleus.factory.RequiresPresenter
|
|||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@RequiresPresenter(ChaptersPresenter::class)
|
@RequiresPresenter(ChaptersPresenter::class)
|
||||||
class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callback, FlexibleViewHolder.OnListItemClickListener {
|
class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(),
|
||||||
|
ActionMode.Callback,
|
||||||
|
FlexibleAdapter.OnItemClickListener,
|
||||||
|
FlexibleAdapter.OnItemLongClickListener {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
@ -70,38 +73,31 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
|
|||||||
recycler.layoutManager = LinearLayoutManager(activity)
|
recycler.layoutManager = LinearLayoutManager(activity)
|
||||||
recycler.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
|
recycler.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
|
||||||
recycler.setHasFixedSize(true)
|
recycler.setHasFixedSize(true)
|
||||||
|
// TODO enable in a future commit
|
||||||
|
// adapter.setFastScroller(fast_scroller, context.getResourceColor(R.attr.colorAccent))
|
||||||
|
// adapter.toggleFastScroller()
|
||||||
|
|
||||||
swipe_refresh.setOnRefreshListener { fetchChapters() }
|
swipe_refresh.setOnRefreshListener { fetchChapters() }
|
||||||
|
|
||||||
fab.setOnClickListener {
|
fab.setOnClickListener {
|
||||||
val chapter = presenter.getNextUnreadChapter()
|
val item = presenter.getNextUnreadChapter()
|
||||||
if (chapter != null) {
|
if (item != null) {
|
||||||
// Create animation listener
|
// Create animation listener
|
||||||
val revealAnimationListener: Animator.AnimatorListener = object : AnimatorListenerAdapter() {
|
val revealAnimationListener: Animator.AnimatorListener = object : AnimatorListenerAdapter() {
|
||||||
override fun onAnimationStart(animation: Animator?) {
|
override fun onAnimationStart(animation: Animator?) {
|
||||||
openChapter(chapter, true)
|
openChapter(item.chapter, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get coordinates and start animation
|
// Get coordinates and start animation
|
||||||
val coordinates = fab.getCoordinates()
|
val coordinates = fab.getCoordinates()
|
||||||
if (!reveal_view.showRevealEffect(coordinates.x, coordinates.y, revealAnimationListener)) {
|
if (!reveal_view.showRevealEffect(coordinates.x, coordinates.y, revealAnimationListener)) {
|
||||||
openChapter(chapter)
|
openChapter(item.chapter)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
context.toast(R.string.no_next_chapter)
|
context.toast(R.string.no_next_chapter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
// Stop recycler's scrolling when onPause is called. If the activity is finishing
|
|
||||||
// the presenter will be destroyed, and it could cause NPE
|
|
||||||
// https://github.com/inorichi/tachiyomi/issues/159
|
|
||||||
recycler.stopScroll()
|
|
||||||
|
|
||||||
super.onPause()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
@ -172,19 +168,20 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
fun onNextManga(manga: Manga) {
|
fun onNextManga(manga: Manga) {
|
||||||
// Set initial values
|
// Set initial values
|
||||||
activity.supportInvalidateOptionsMenu()
|
activity.supportInvalidateOptionsMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onNextChapters(chapters: List<ChapterModel>) {
|
fun onNextChapters(chapters: List<ChapterItem>) {
|
||||||
// If the list is empty, fetch chapters from source if the conditions are met
|
// If the list is empty, fetch chapters from source if the conditions are met
|
||||||
// We use presenter chapters instead because they are always unfiltered
|
// We use presenter chapters instead because they are always unfiltered
|
||||||
if (presenter.chapters.isEmpty())
|
if (presenter.chapters.isEmpty())
|
||||||
initialFetchChapters()
|
initialFetchChapters()
|
||||||
|
|
||||||
destroyActionModeIfNeeded()
|
destroyActionModeIfNeeded()
|
||||||
adapter.items = chapters
|
adapter.updateDataSet(chapters)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initialFetchChapters() {
|
private fun initialFetchChapters() {
|
||||||
@ -229,7 +226,7 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
|
|||||||
.title(R.string.action_display_mode)
|
.title(R.string.action_display_mode)
|
||||||
.items(modes.map { getString(it) })
|
.items(modes.map { getString(it) })
|
||||||
.itemsIds(ids)
|
.itemsIds(ids)
|
||||||
.itemsCallbackSingleChoice(selectedIndex) { dialog, itemView, which, text ->
|
.itemsCallbackSingleChoice(selectedIndex) { _, itemView, _, _ ->
|
||||||
// Save the new display mode
|
// Save the new display mode
|
||||||
presenter.setDisplayMode(itemView.id)
|
presenter.setDisplayMode(itemView.id)
|
||||||
// Refresh ui
|
// Refresh ui
|
||||||
@ -249,7 +246,7 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
|
|||||||
.title(R.string.sorting_mode)
|
.title(R.string.sorting_mode)
|
||||||
.items(modes.map { getString(it) })
|
.items(modes.map { getString(it) })
|
||||||
.itemsIds(ids)
|
.itemsIds(ids)
|
||||||
.itemsCallbackSingleChoice(selectedIndex) { dialog, itemView, which, text ->
|
.itemsCallbackSingleChoice(selectedIndex) { _, itemView, _, _ ->
|
||||||
// Save the new sorting mode
|
// Save the new sorting mode
|
||||||
presenter.setSorting(itemView.id)
|
presenter.setSorting(itemView.id)
|
||||||
true
|
true
|
||||||
@ -266,7 +263,7 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
|
|||||||
.title(R.string.manga_download)
|
.title(R.string.manga_download)
|
||||||
.negativeText(android.R.string.cancel)
|
.negativeText(android.R.string.cancel)
|
||||||
.items(modes.map { getString(it) })
|
.items(modes.map { getString(it) })
|
||||||
.itemsCallback { dialog, view, i, charSequence ->
|
.itemsCallback { _, _, i, _ ->
|
||||||
|
|
||||||
fun getUnreadChaptersSorted() = presenter.chapters
|
fun getUnreadChaptersSorted() = presenter.chapters
|
||||||
.filter { !it.read && it.status == Download.NOT_DOWNLOADED }
|
.filter { !it.read && it.status == Download.NOT_DOWNLOADED }
|
||||||
@ -298,8 +295,8 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
|
|||||||
getHolder(download.chapter)?.notifyStatus(download.status)
|
getHolder(download.chapter)?.notifyStatus(download.status)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getHolder(chapter: Chapter): ChaptersHolder? {
|
private fun getHolder(chapter: Chapter): ChapterHolder? {
|
||||||
return recycler.findViewHolderForItemId(chapter.id!!) as? ChaptersHolder
|
return recycler.findViewHolderForItemId(chapter.id!!) as? ChapterHolder
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
@ -323,7 +320,7 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
|
|||||||
.content(R.string.confirm_delete_chapters)
|
.content(R.string.confirm_delete_chapters)
|
||||||
.positiveText(android.R.string.yes)
|
.positiveText(android.R.string.yes)
|
||||||
.negativeText(android.R.string.no)
|
.negativeText(android.R.string.no)
|
||||||
.onPositive { dialog, action -> deleteChapters(getSelectedChapters()) }
|
.onPositive { _, _ -> deleteChapters(getSelectedChapters()) }
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
else -> return false
|
else -> return false
|
||||||
@ -337,8 +334,8 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
|
|||||||
actionMode = null
|
actionMode = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSelectedChapters(): List<ChapterModel> {
|
fun getSelectedChapters(): List<ChapterItem> {
|
||||||
return adapter.selectedItems.map { adapter.getItem(it) }
|
return adapter.selectedPositions.map { adapter.getItem(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun destroyActionModeIfNeeded() {
|
fun destroyActionModeIfNeeded() {
|
||||||
@ -350,18 +347,18 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
|
|||||||
setContextTitle(adapter.selectedItemCount)
|
setContextTitle(adapter.selectedItemCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun markAsRead(chapters: List<ChapterModel>) {
|
fun markAsRead(chapters: List<ChapterItem>) {
|
||||||
presenter.markChaptersRead(chapters, true)
|
presenter.markChaptersRead(chapters, true)
|
||||||
if (presenter.preferences.removeAfterMarkedAsRead()) {
|
if (presenter.preferences.removeAfterMarkedAsRead()) {
|
||||||
deleteChapters(chapters)
|
deleteChapters(chapters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun markAsUnread(chapters: List<ChapterModel>) {
|
fun markAsUnread(chapters: List<ChapterItem>) {
|
||||||
presenter.markChaptersRead(chapters, false)
|
presenter.markChaptersRead(chapters, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun markPreviousAsRead(chapter: ChapterModel) {
|
fun markPreviousAsRead(chapter: ChapterItem) {
|
||||||
val chapters = if (presenter.sortDescending()) adapter.items.reversed() else adapter.items
|
val chapters = if (presenter.sortDescending()) adapter.items.reversed() else adapter.items
|
||||||
val chapterPos = chapters.indexOf(chapter)
|
val chapterPos = chapters.indexOf(chapter)
|
||||||
if (chapterPos != -1) {
|
if (chapterPos != -1) {
|
||||||
@ -369,7 +366,7 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun downloadChapters(chapters: List<ChapterModel>) {
|
fun downloadChapters(chapters: List<ChapterItem>) {
|
||||||
destroyActionModeIfNeeded()
|
destroyActionModeIfNeeded()
|
||||||
presenter.downloadChapters(chapters)
|
presenter.downloadChapters(chapters)
|
||||||
if (!presenter.manga.favorite){
|
if (!presenter.manga.favorite){
|
||||||
@ -381,12 +378,12 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bookmarkChapters(chapters: List<ChapterModel>, bookmarked: Boolean) {
|
fun bookmarkChapters(chapters: List<ChapterItem>, bookmarked: Boolean) {
|
||||||
destroyActionModeIfNeeded()
|
destroyActionModeIfNeeded()
|
||||||
presenter.bookmarkChapters(chapters, bookmarked)
|
presenter.bookmarkChapters(chapters, bookmarked)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteChapters(chapters: List<ChapterModel>) {
|
fun deleteChapters(chapters: List<ChapterItem>) {
|
||||||
destroyActionModeIfNeeded()
|
destroyActionModeIfNeeded()
|
||||||
DeletingChaptersDialog().show(childFragmentManager, DeletingChaptersDialog.TAG)
|
DeletingChaptersDialog().show(childFragmentManager, DeletingChaptersDialog.TAG)
|
||||||
presenter.deleteChapters(chapters)
|
presenter.deleteChapters(chapters)
|
||||||
@ -407,26 +404,40 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
|
|||||||
?.dismissAllowingStateLoss()
|
?.dismissAllowingStateLoss()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onListItemClick(position: Int): Boolean {
|
override fun onItemClick(position: Int): Boolean {
|
||||||
val item = adapter.getItem(position) ?: return false
|
val item = adapter.getItem(position) ?: return false
|
||||||
if (actionMode != null && adapter.mode == FlexibleAdapter.MODE_MULTI) {
|
if (actionMode != null && adapter.mode == FlexibleAdapter.MODE_MULTI) {
|
||||||
toggleSelection(position)
|
toggleSelection(position)
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
openChapter(item)
|
openChapter(item.chapter)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onListItemLongClick(position: Int) {
|
override fun onItemLongClick(position: Int) {
|
||||||
if (actionMode == null)
|
if (actionMode == null)
|
||||||
actionMode = activity.startSupportActionMode(this)
|
actionMode = (activity as AppCompatActivity).startSupportActionMode(this)
|
||||||
|
|
||||||
toggleSelection(position)
|
toggleSelection(position)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onItemMenuClick(position: Int, item: MenuItem) {
|
||||||
|
val chapter = adapter.getItem(position)?.let { listOf(it) } ?: return
|
||||||
|
|
||||||
|
when (item.itemId) {
|
||||||
|
R.id.action_download -> downloadChapters(chapter)
|
||||||
|
R.id.action_bookmark -> bookmarkChapters(chapter, true)
|
||||||
|
R.id.action_remove_bookmark -> bookmarkChapters(chapter, false)
|
||||||
|
R.id.action_delete -> deleteChapters(chapter)
|
||||||
|
R.id.action_mark_as_read -> markAsRead(chapter)
|
||||||
|
R.id.action_mark_as_unread -> markAsUnread(chapter)
|
||||||
|
R.id.action_mark_previous_as_read -> markPreviousAsRead(chapter[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun toggleSelection(position: Int) {
|
private fun toggleSelection(position: Int) {
|
||||||
adapter.toggleSelection(position, false)
|
adapter.toggleSelection(position)
|
||||||
|
|
||||||
val count = adapter.selectedItemCount
|
val count = adapter.selectedItemCount
|
||||||
if (count == 0) {
|
if (count == 0) {
|
||||||
|
@ -65,14 +65,14 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
|
|||||||
/**
|
/**
|
||||||
* List of chapters of the manga. It's always unfiltered and unsorted.
|
* List of chapters of the manga. It's always unfiltered and unsorted.
|
||||||
*/
|
*/
|
||||||
var chapters: List<ChapterModel> = emptyList()
|
var chapters: List<ChapterItem> = emptyList()
|
||||||
private set
|
private set
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subject of list of chapters to allow updating the view without going to DB.
|
* Subject of list of chapters to allow updating the view without going to DB.
|
||||||
*/
|
*/
|
||||||
val chaptersRelay: PublishRelay<List<ChapterModel>>
|
val chaptersRelay: PublishRelay<List<ChapterItem>>
|
||||||
by lazy { PublishRelay.create<List<ChapterModel>>() }
|
by lazy { PublishRelay.create<List<ChapterItem>>() }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the chapter list has been requested to the source.
|
* Whether the chapter list has been requested to the source.
|
||||||
@ -103,7 +103,7 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
|
|||||||
chaptersRelay.flatMap { applyChapterFilters(it) }
|
chaptersRelay.flatMap { applyChapterFilters(it) }
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribeLatestCache(ChaptersFragment::onNextChapters,
|
.subscribeLatestCache(ChaptersFragment::onNextChapters,
|
||||||
{ view, error -> Timber.e(error) })
|
{ _, error -> Timber.e(error) })
|
||||||
|
|
||||||
// Add the subscription that retrieves the chapters from the database, keeps subscribed to
|
// Add the subscription that retrieves the chapters from the database, keeps subscribed to
|
||||||
// changes, and sends the list of chapters to the relay.
|
// changes, and sends the list of chapters to the relay.
|
||||||
@ -135,15 +135,15 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
|
|||||||
.filter { download -> download.manga.id == manga.id }
|
.filter { download -> download.manga.id == manga.id }
|
||||||
.doOnNext { onDownloadStatusChange(it) }
|
.doOnNext { onDownloadStatusChange(it) }
|
||||||
.subscribeLatestCache(ChaptersFragment::onChapterStatusChange,
|
.subscribeLatestCache(ChaptersFragment::onChapterStatusChange,
|
||||||
{ view, error -> Timber.e(error) })
|
{ _, error -> Timber.e(error) })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a chapter from the database to an extended model, allowing to store new fields.
|
* Converts a chapter from the database to an extended model, allowing to store new fields.
|
||||||
*/
|
*/
|
||||||
private fun Chapter.toModel(): ChapterModel {
|
private fun Chapter.toModel(): ChapterItem {
|
||||||
// Create the model object.
|
// Create the model object.
|
||||||
val model = ChapterModel(this)
|
val model = ChapterItem(this, manga)
|
||||||
|
|
||||||
// Find an active download for this chapter.
|
// Find an active download for this chapter.
|
||||||
val download = downloadManager.queue.find { it.chapter.id == id }
|
val download = downloadManager.queue.find { it.chapter.id == id }
|
||||||
@ -160,7 +160,7 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
|
|||||||
*
|
*
|
||||||
* @param chapters the list of chapter from the database.
|
* @param chapters the list of chapter from the database.
|
||||||
*/
|
*/
|
||||||
private fun setDownloadedChapters(chapters: List<ChapterModel>) {
|
private fun setDownloadedChapters(chapters: List<ChapterItem>) {
|
||||||
val files = downloadManager.findMangaDir(source, manga)?.listFiles() ?: return
|
val files = downloadManager.findMangaDir(source, manga)?.listFiles() ?: return
|
||||||
val cached = mutableMapOf<Chapter, String>()
|
val cached = mutableMapOf<Chapter, String>()
|
||||||
files.mapNotNull { it.name }
|
files.mapNotNull { it.name }
|
||||||
@ -181,7 +181,7 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
|
|||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.map { syncChaptersWithSource(db, it, manga, source) }
|
.map { syncChaptersWithSource(db, it, manga, source) }
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribeFirst({ view, chapters ->
|
.subscribeFirst({ view, _ ->
|
||||||
view.onFetchChaptersDone()
|
view.onFetchChaptersDone()
|
||||||
}, ChaptersFragment::onFetchChaptersError)
|
}, ChaptersFragment::onFetchChaptersError)
|
||||||
}
|
}
|
||||||
@ -198,7 +198,7 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
|
|||||||
* @param chapters the list of chapters from the database
|
* @param chapters the list of chapters from the database
|
||||||
* @return an observable of the list of chapters filtered and sorted.
|
* @return an observable of the list of chapters filtered and sorted.
|
||||||
*/
|
*/
|
||||||
private fun applyChapterFilters(chapters: List<ChapterModel>): Observable<List<ChapterModel>> {
|
private fun applyChapterFilters(chapters: List<ChapterItem>): Observable<List<ChapterItem>> {
|
||||||
var observable = Observable.from(chapters).subscribeOn(Schedulers.io())
|
var observable = Observable.from(chapters).subscribeOn(Schedulers.io())
|
||||||
if (onlyUnread()) {
|
if (onlyUnread()) {
|
||||||
observable = observable.filter { !it.read }
|
observable = observable.filter { !it.read }
|
||||||
@ -248,7 +248,7 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
|
|||||||
/**
|
/**
|
||||||
* Returns the next unread chapter or null if everything is read.
|
* Returns the next unread chapter or null if everything is read.
|
||||||
*/
|
*/
|
||||||
fun getNextUnreadChapter(): ChapterModel? {
|
fun getNextUnreadChapter(): ChapterItem? {
|
||||||
return chapters.sortedByDescending { it.source_order }.find { !it.read }
|
return chapters.sortedByDescending { it.source_order }.find { !it.read }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -257,7 +257,7 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
|
|||||||
* @param selectedChapters the list of selected chapters.
|
* @param selectedChapters the list of selected chapters.
|
||||||
* @param read whether to mark chapters as read or unread.
|
* @param read whether to mark chapters as read or unread.
|
||||||
*/
|
*/
|
||||||
fun markChaptersRead(selectedChapters: List<ChapterModel>, read: Boolean) {
|
fun markChaptersRead(selectedChapters: List<ChapterItem>, read: Boolean) {
|
||||||
Observable.from(selectedChapters)
|
Observable.from(selectedChapters)
|
||||||
.doOnNext { chapter ->
|
.doOnNext { chapter ->
|
||||||
chapter.read = read
|
chapter.read = read
|
||||||
@ -275,7 +275,7 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
|
|||||||
* Downloads the given list of chapters with the manager.
|
* Downloads the given list of chapters with the manager.
|
||||||
* @param chapters the list of chapters to download.
|
* @param chapters the list of chapters to download.
|
||||||
*/
|
*/
|
||||||
fun downloadChapters(chapters: List<ChapterModel>) {
|
fun downloadChapters(chapters: List<ChapterItem>) {
|
||||||
DownloadService.start(context)
|
DownloadService.start(context)
|
||||||
downloadManager.downloadChapters(manga, chapters)
|
downloadManager.downloadChapters(manga, chapters)
|
||||||
}
|
}
|
||||||
@ -284,7 +284,7 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
|
|||||||
* Bookmarks the given list of chapters.
|
* Bookmarks the given list of chapters.
|
||||||
* @param selectedChapters the list of chapters to bookmark.
|
* @param selectedChapters the list of chapters to bookmark.
|
||||||
*/
|
*/
|
||||||
fun bookmarkChapters(selectedChapters: List<ChapterModel>, bookmarked: Boolean) {
|
fun bookmarkChapters(selectedChapters: List<ChapterItem>, bookmarked: Boolean) {
|
||||||
Observable.from(selectedChapters)
|
Observable.from(selectedChapters)
|
||||||
.doOnNext { chapter ->
|
.doOnNext { chapter ->
|
||||||
chapter.bookmark = bookmarked
|
chapter.bookmark = bookmarked
|
||||||
@ -299,14 +299,14 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
|
|||||||
* Deletes the given list of chapter.
|
* Deletes the given list of chapter.
|
||||||
* @param chapters the list of chapters to delete.
|
* @param chapters the list of chapters to delete.
|
||||||
*/
|
*/
|
||||||
fun deleteChapters(chapters: List<ChapterModel>) {
|
fun deleteChapters(chapters: List<ChapterItem>) {
|
||||||
Observable.from(chapters)
|
Observable.from(chapters)
|
||||||
.doOnNext { deleteChapter(it) }
|
.doOnNext { deleteChapter(it) }
|
||||||
.toList()
|
.toList()
|
||||||
.doOnNext { if (onlyDownloaded()) refreshChapters() }
|
.doOnNext { if (onlyDownloaded()) refreshChapters() }
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribeFirst({ view, result ->
|
.subscribeFirst({ view, _ ->
|
||||||
view.onChaptersDeleted()
|
view.onChaptersDeleted()
|
||||||
}, ChaptersFragment::onChaptersDeletedError)
|
}, ChaptersFragment::onChaptersDeletedError)
|
||||||
}
|
}
|
||||||
@ -315,7 +315,7 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
|
|||||||
* Deletes a chapter from disk. This method is called in a background thread.
|
* Deletes a chapter from disk. This method is called in a background thread.
|
||||||
* @param chapter the chapter to delete.
|
* @param chapter the chapter to delete.
|
||||||
*/
|
*/
|
||||||
private fun deleteChapter(chapter: ChapterModel) {
|
private fun deleteChapter(chapter: ChapterItem) {
|
||||||
downloadManager.queue.remove(chapter)
|
downloadManager.queue.remove(chapter)
|
||||||
downloadManager.deleteChapter(source, manga, chapter)
|
downloadManager.deleteChapter(source, manga, chapter)
|
||||||
chapter.status = Download.NOT_DOWNLOADED
|
chapter.status = Download.NOT_DOWNLOADED
|
||||||
|
@ -6,6 +6,7 @@ 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 android.widget.Toast
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import com.bumptech.glide.BitmapRequestBuilder
|
import com.bumptech.glide.BitmapRequestBuilder
|
||||||
import com.bumptech.glide.BitmapTypeRequest
|
import com.bumptech.glide.BitmapTypeRequest
|
||||||
@ -14,6 +15,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
|
|||||||
import com.bumptech.glide.load.resource.bitmap.CenterCrop
|
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.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
@ -31,6 +33,7 @@ import nucleus.factory.RequiresPresenter
|
|||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fragment that shows manga information.
|
* Fragment that shows manga information.
|
||||||
@ -52,6 +55,11 @@ class MangaInfoFragment : BaseRxFragment<MangaInfoPresenter>() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preferences helper.
|
||||||
|
*/
|
||||||
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
setHasOptionsMenu(true)
|
setHasOptionsMenu(true)
|
||||||
@ -63,7 +71,19 @@ class MangaInfoFragment : BaseRxFragment<MangaInfoPresenter>() {
|
|||||||
|
|
||||||
override fun onViewCreated(view: View?, savedState: Bundle?) {
|
override fun onViewCreated(view: View?, savedState: Bundle?) {
|
||||||
// Set onclickListener to toggle favorite when FAB clicked.
|
// Set onclickListener to toggle favorite when FAB clicked.
|
||||||
fab_favorite.setOnClickListener { toggleFavorite() }
|
fab_favorite.setOnClickListener {
|
||||||
|
if(!presenter.manga.favorite) {
|
||||||
|
val defaultCategory = presenter.getCategories().find { it.id == preferences.defaultCategory()}
|
||||||
|
if(defaultCategory == null) {
|
||||||
|
onFabClick()
|
||||||
|
} else {
|
||||||
|
toggleFavorite()
|
||||||
|
presenter.moveMangaToCategory(defaultCategory, presenter.manga)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toggleFavorite()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Set SwipeRefresh to refresh manga data.
|
// Set SwipeRefresh to refresh manga data.
|
||||||
swipe_refresh.setOnRefreshListener { fetchMangaFromSource() }
|
swipe_refresh.setOnRefreshListener { fetchMangaFromSource() }
|
||||||
@ -334,4 +354,40 @@ class MangaInfoFragment : BaseRxFragment<MangaInfoPresenter>() {
|
|||||||
swipe_refresh.isRefreshing = value
|
swipe_refresh.isRefreshing = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the fab is clicked.
|
||||||
|
*/
|
||||||
|
private fun onFabClick() {
|
||||||
|
val categories = presenter.getCategories()
|
||||||
|
|
||||||
|
MaterialDialog.Builder(activity)
|
||||||
|
.title(R.string.action_move_category)
|
||||||
|
.items(categories.map { it.name })
|
||||||
|
.itemsCallbackMultiChoice(presenter.getMangaCategoryIds(presenter.manga)) { dialog, position, text ->
|
||||||
|
if (position.contains(0) && position.count() > 1) {
|
||||||
|
dialog.setSelectedIndices(position.filter {it > 0}.toTypedArray())
|
||||||
|
Toast.makeText(dialog.context, R.string.invalid_combination, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
.alwaysCallMultiChoiceCallback()
|
||||||
|
.positiveText(android.R.string.ok)
|
||||||
|
.negativeText(android.R.string.cancel)
|
||||||
|
.onPositive { dialog, _ ->
|
||||||
|
val selectedCategories = dialog.selectedIndices?.map { categories[it] } ?: emptyList()
|
||||||
|
|
||||||
|
if(!selectedCategories.isEmpty()) {
|
||||||
|
if(!presenter.manga.favorite) {
|
||||||
|
toggleFavorite()
|
||||||
|
}
|
||||||
|
presenter.moveMangaToCategories(selectedCategories.filter { it.id != 0}, presenter.manga)
|
||||||
|
} else {
|
||||||
|
toggleFavorite()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,9 @@ package eu.kanade.tachiyomi.ui.manga.info
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
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.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
@ -76,9 +78,12 @@ class MangaInfoPresenter : BasePresenter<MangaInfoFragment>() {
|
|||||||
?.subscribeLatestCache(MangaInfoFragment::setChapterCount)
|
?.subscribeLatestCache(MangaInfoFragment::setChapterCount)
|
||||||
|
|
||||||
// Update favorite status
|
// Update favorite status
|
||||||
SharedData.get(MangaFavoriteEvent::class.java)?.observable
|
SharedData.get(MangaFavoriteEvent::class.java)?.let {
|
||||||
?.observeOn(AndroidSchedulers.mainThread())
|
it.observable
|
||||||
?.subscribe { setFavorite(it) }
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe { setFavorite(it) }
|
||||||
|
.apply { add(this) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -148,4 +153,49 @@ class MangaInfoPresenter : BasePresenter<MangaInfoFragment>() {
|
|||||||
downloadManager.findMangaDir(source, manga)?.delete()
|
downloadManager.findMangaDir(source, manga)?.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the default, and user categories.
|
||||||
|
*
|
||||||
|
* @return List of categories, default plus user categories
|
||||||
|
*/
|
||||||
|
fun getCategories(): List<Category> {
|
||||||
|
return arrayListOf(Category.createDefault()) + db.getCategories().executeAsBlocking()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the category id's the manga is in, if the manga is not in a category, returns the default id.
|
||||||
|
*
|
||||||
|
* @param manga the manga to get categories from.
|
||||||
|
* @return Array of category ids the manga is in, if none returns default id
|
||||||
|
*/
|
||||||
|
fun getMangaCategoryIds(manga: Manga): Array<Int?> {
|
||||||
|
val categories = db.getCategoriesForManga(manga).executeAsBlocking()
|
||||||
|
if(categories.isEmpty()) {
|
||||||
|
return arrayListOf(Category.createDefault().id).toTypedArray()
|
||||||
|
}
|
||||||
|
return categories.map { it.id }.toTypedArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the given manga to categories.
|
||||||
|
*
|
||||||
|
* @param categories the selected categories.
|
||||||
|
* @param manga the manga to move.
|
||||||
|
*/
|
||||||
|
fun moveMangaToCategories(categories: List<Category>, manga: Manga) {
|
||||||
|
val mc = categories.map { MangaCategory.create(manga, it) }
|
||||||
|
|
||||||
|
db.setMangaCategories(mc, arrayListOf(manga))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the given manga to the category.
|
||||||
|
*
|
||||||
|
* @param category the selected category.
|
||||||
|
* @param manga the manga to move.
|
||||||
|
*/
|
||||||
|
fun moveMangaToCategory(category: Category, manga: Manga) {
|
||||||
|
moveMangaToCategories(arrayListOf(category), manga)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -607,13 +607,17 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DiskUtil.scanMedia(context, destFile)
|
||||||
|
|
||||||
imageNotifier.onComplete(destFile)
|
imageNotifier.onComplete(destFile)
|
||||||
}
|
}
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.subscribe({},
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
{ error ->
|
.subscribe({
|
||||||
Timber.e(error)
|
context.toast(R.string.picture_saved)
|
||||||
imageNotifier.onError(error.message)
|
}, { error ->
|
||||||
})
|
Timber.e(error)
|
||||||
|
imageNotifier.onError(error.message)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -230,7 +230,10 @@ abstract class PagerReader : BaseReader() {
|
|||||||
*/
|
*/
|
||||||
protected fun setPagesOnAdapter() {
|
protected fun setPagesOnAdapter() {
|
||||||
if (pages.isNotEmpty()) {
|
if (pages.isNotEmpty()) {
|
||||||
|
// Prevent a wrong active page when changing chapters with the navigation buttons.
|
||||||
|
val currPage = currentPage
|
||||||
adapter.pages = pages
|
adapter.pages = pages
|
||||||
|
currentPage = currPage
|
||||||
if (currentPage == pager.currentItem) {
|
if (currentPage == pager.currentItem) {
|
||||||
onPageChanged(currentPage)
|
onPageChanged(currentPage)
|
||||||
} else {
|
} else {
|
||||||
|
@ -76,10 +76,10 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
view.progress_container.minimumHeight = view.resources.displayMetrics.heightPixels * 2
|
view.progress_container.minimumHeight = webtoonReader.screenHeight
|
||||||
|
|
||||||
view.setOnTouchListener(adapter.touchListener)
|
view.setOnTouchListener(adapter.touchListener)
|
||||||
view.retry_button.setOnTouchListener { v, event ->
|
view.retry_button.setOnTouchListener { _, event ->
|
||||||
if (event.action == MotionEvent.ACTION_UP) {
|
if (event.action == MotionEvent.ACTION_UP) {
|
||||||
readerActivity.presenter.retryPage(page)
|
readerActivity.presenter.retryPage(page)
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
|
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.v7.widget.RecyclerView
|
import android.support.v7.widget.RecyclerView
|
||||||
|
import android.util.DisplayMetrics
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
@ -64,6 +66,19 @@ class WebtoonReader : BaseReader() {
|
|||||||
|
|
||||||
private var scrollDistance: Int = 0
|
private var scrollDistance: Int = 0
|
||||||
|
|
||||||
|
val screenHeight by lazy {
|
||||||
|
val display = activity.windowManager.defaultDisplay
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||||
|
val metrics = DisplayMetrics()
|
||||||
|
display.getRealMetrics(metrics)
|
||||||
|
metrics.heightPixels
|
||||||
|
} else {
|
||||||
|
val field = Display::class.java.getMethod("getRawHeight")
|
||||||
|
field.invoke(display) as Int
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? {
|
||||||
adapter = WebtoonAdapter(this)
|
adapter = WebtoonAdapter(this)
|
||||||
|
|
||||||
@ -72,9 +87,6 @@ class WebtoonReader : BaseReader() {
|
|||||||
|
|
||||||
layoutManager = PreCachingLayoutManager(activity)
|
layoutManager = PreCachingLayoutManager(activity)
|
||||||
layoutManager.extraLayoutSpace = screenHeight / 2
|
layoutManager.extraLayoutSpace = screenHeight / 2
|
||||||
if (savedState != null) {
|
|
||||||
layoutManager.scrollToPositionWithOffset(savedState.getInt(SAVED_POSITION), 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
recycler = RecyclerView(activity).apply {
|
recycler = RecyclerView(activity).apply {
|
||||||
layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
||||||
@ -107,6 +119,26 @@ class WebtoonReader : BaseReader() {
|
|||||||
return recycler
|
return recycler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses two ways to scroll to the last page read.
|
||||||
|
*/
|
||||||
|
private fun scrollToLastPageRead(page: Int) {
|
||||||
|
// Scrolls to the correct page initially, but isn't reliable beyond that.
|
||||||
|
recycler.addOnLayoutChangeListener(object: View.OnLayoutChangeListener {
|
||||||
|
override fun onLayoutChange(p0: View?, p1: Int, p2: Int, p3: Int, p4: Int, p5: Int, p6: Int, p7: Int, p8: Int) {
|
||||||
|
if(pages.isEmpty()) {
|
||||||
|
setActivePage(page)
|
||||||
|
} else {
|
||||||
|
recycler.removeOnLayoutChangeListener(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Scrolls to the correct page after app has been in use, but can't do it the very first time.
|
||||||
|
recycler.post { setActivePage(page) }
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
subscriptions.unsubscribe()
|
subscriptions.unsubscribe()
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
@ -150,6 +182,7 @@ class WebtoonReader : BaseReader() {
|
|||||||
// Make sure the view is already initialized.
|
// Make sure the view is already initialized.
|
||||||
if (view != null) {
|
if (view != null) {
|
||||||
setPagesOnAdapter()
|
setPagesOnAdapter()
|
||||||
|
scrollToLastPageRead(this.currentPage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.recent_updates
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.v4.app.DialogFragment
|
import android.support.v4.app.DialogFragment
|
||||||
|
import android.support.v7.app.AppCompatActivity
|
||||||
import android.support.v7.view.ActionMode
|
import android.support.v7.view.ActionMode
|
||||||
import android.support.v7.widget.DividerItemDecoration
|
import android.support.v7.widget.DividerItemDecoration
|
||||||
import android.support.v7.widget.LinearLayoutManager
|
import android.support.v7.widget.LinearLayoutManager
|
||||||
@ -142,7 +143,7 @@ class RecentChaptersFragment:
|
|||||||
*/
|
*/
|
||||||
override fun onItemLongClick(position: Int) {
|
override fun onItemLongClick(position: Int) {
|
||||||
if (actionMode == null)
|
if (actionMode == null)
|
||||||
actionMode = activity.startSupportActionMode(this)
|
actionMode = (activity as AppCompatActivity).startSupportActionMode(this)
|
||||||
|
|
||||||
toggleSelection(position)
|
toggleSelection(position)
|
||||||
}
|
}
|
||||||
|
@ -65,6 +65,7 @@ class SettingsActivity : BaseActivity(),
|
|||||||
"downloads_screen" -> SettingsDownloadsFragment.newInstance(key)
|
"downloads_screen" -> SettingsDownloadsFragment.newInstance(key)
|
||||||
"sources_screen" -> SettingsSourcesFragment.newInstance(key)
|
"sources_screen" -> SettingsSourcesFragment.newInstance(key)
|
||||||
"tracking_screen" -> SettingsTrackingFragment.newInstance(key)
|
"tracking_screen" -> SettingsTrackingFragment.newInstance(key)
|
||||||
|
"backup_screen" -> SettingsBackupFragment.newInstance(key)
|
||||||
"advanced_screen" -> SettingsAdvancedFragment.newInstance(key)
|
"advanced_screen" -> SettingsAdvancedFragment.newInstance(key)
|
||||||
"about_screen" -> SettingsAboutFragment.newInstance(key)
|
"about_screen" -> SettingsAboutFragment.newInstance(key)
|
||||||
else -> SettingsFragment.newInstance(key)
|
else -> SettingsFragment.newInstance(key)
|
||||||
|
@ -108,6 +108,7 @@ class SettingsAdvancedFragment : SettingsFragment() {
|
|||||||
.onPositive { dialog, which ->
|
.onPositive { dialog, which ->
|
||||||
(activity as SettingsActivity).parentFlags = SettingsActivity.FLAG_DATABASE_CLEARED
|
(activity as SettingsActivity).parentFlags = SettingsActivity.FLAG_DATABASE_CLEARED
|
||||||
db.deleteMangasNotInLibrary().executeAsBlocking()
|
db.deleteMangasNotInLibrary().executeAsBlocking()
|
||||||
|
db.deleteHistoryNoLastRead().executeAsBlocking()
|
||||||
activity.toast(R.string.clear_database_completed)
|
activity.toast(R.string.clear_database_completed)
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
|
@ -0,0 +1,422 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.setting
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.v7.preference.XpPreferenceFragment
|
||||||
|
import android.view.View
|
||||||
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
|
import com.nononsenseapps.filepicker.FilePickerActivity
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
|
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||||
|
import eu.kanade.tachiyomi.util.*
|
||||||
|
import eu.kanade.tachiyomi.widget.CustomLayoutPickerActivity
|
||||||
|
import eu.kanade.tachiyomi.widget.preference.IntListPreference
|
||||||
|
import net.xpece.android.support.preference.Preference
|
||||||
|
import rx.subscriptions.Subscriptions
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.File
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings for [BackupCreateService] and [BackupRestoreService]
|
||||||
|
*/
|
||||||
|
class SettingsBackupFragment : SettingsFragment() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val INTENT_FILTER = "SettingsBackupFragment"
|
||||||
|
const val ACTION_BACKUP_COMPLETED_DIALOG = "$ID.$INTENT_FILTER.ACTION_BACKUP_COMPLETED_DIALOG"
|
||||||
|
const val ACTION_SET_PROGRESS_DIALOG = "$ID.$INTENT_FILTER.ACTION_SET_PROGRESS_DIALOG"
|
||||||
|
const val ACTION_ERROR_BACKUP_DIALOG = "$ID.$INTENT_FILTER.ACTION_ERROR_BACKUP_DIALOG"
|
||||||
|
const val ACTION_ERROR_RESTORE_DIALOG = "$ID.$INTENT_FILTER.ACTION_ERROR_RESTORE_DIALOG"
|
||||||
|
const val ACTION_RESTORE_COMPLETED_DIALOG = "$ID.$INTENT_FILTER.ACTION_RESTORE_COMPLETED_DIALOG"
|
||||||
|
const val ACTION = "$ID.$INTENT_FILTER.ACTION"
|
||||||
|
const val EXTRA_PROGRESS = "$ID.$INTENT_FILTER.EXTRA_PROGRESS"
|
||||||
|
const val EXTRA_AMOUNT = "$ID.$INTENT_FILTER.EXTRA_AMOUNT"
|
||||||
|
const val EXTRA_ERRORS = "$ID.$INTENT_FILTER.EXTRA_ERRORS"
|
||||||
|
const val EXTRA_CONTENT = "$ID.$INTENT_FILTER.EXTRA_CONTENT"
|
||||||
|
const val EXTRA_ERROR_MESSAGE = "$ID.$INTENT_FILTER.EXTRA_ERROR_MESSAGE"
|
||||||
|
const val EXTRA_URI = "$ID.$INTENT_FILTER.EXTRA_URI"
|
||||||
|
const val EXTRA_TIME = "$ID.$INTENT_FILTER.EXTRA_TIME"
|
||||||
|
const val EXTRA_ERROR_FILE_PATH = "$ID.$INTENT_FILTER.EXTRA_ERROR_FILE_PATH"
|
||||||
|
const val EXTRA_ERROR_FILE = "$ID.$INTENT_FILTER.EXTRA_ERROR_FILE"
|
||||||
|
|
||||||
|
private const val BACKUP_CREATE = 201
|
||||||
|
private const val BACKUP_RESTORE = 202
|
||||||
|
private const val BACKUP_DIR = 203
|
||||||
|
|
||||||
|
fun newInstance(rootKey: String): SettingsBackupFragment {
|
||||||
|
val args = Bundle()
|
||||||
|
args.putString(XpPreferenceFragment.ARG_PREFERENCE_ROOT, rootKey)
|
||||||
|
return SettingsBackupFragment().apply { arguments = args }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preference selected to create backup
|
||||||
|
*/
|
||||||
|
private val createBackup: Preference by bindPref(R.string.pref_create_local_backup_key)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preference selected to restore backup
|
||||||
|
*/
|
||||||
|
private val restoreBackup: Preference by bindPref(R.string.pref_restore_local_backup_key)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preference which determines the frequency of automatic backups.
|
||||||
|
*/
|
||||||
|
private val automaticBackup: IntListPreference by bindPref(R.string.pref_backup_interval_key)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preference containing number of automatic backups
|
||||||
|
*/
|
||||||
|
private val backupSlots: IntListPreference by bindPref(R.string.pref_backup_slots_key)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preference containing interval of automatic backups
|
||||||
|
*/
|
||||||
|
private val backupDirPref: Preference by bindPref(R.string.pref_backup_directory_key)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preferences
|
||||||
|
*/
|
||||||
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Value containing information on what to backup
|
||||||
|
*/
|
||||||
|
private var backup_flags = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The root directory for backups..
|
||||||
|
*/
|
||||||
|
private var backupDir = preferences.backupsDirectory().getOrDefault().let {
|
||||||
|
UniFile.fromUri(context, Uri.parse(it))
|
||||||
|
}
|
||||||
|
|
||||||
|
val restoreDialog: MaterialDialog by lazy {
|
||||||
|
MaterialDialog.Builder(context)
|
||||||
|
.title(R.string.backup)
|
||||||
|
.content(R.string.restoring_backup)
|
||||||
|
.progress(false, 100, true)
|
||||||
|
.cancelable(false)
|
||||||
|
.negativeText(R.string.action_stop)
|
||||||
|
.onNegative { materialDialog, _ ->
|
||||||
|
BackupRestoreService.stop(context)
|
||||||
|
materialDialog.dismiss()
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
val backupDialog: MaterialDialog by lazy {
|
||||||
|
MaterialDialog.Builder(context)
|
||||||
|
.title(R.string.backup)
|
||||||
|
.content(R.string.creating_backup)
|
||||||
|
.progress(true, 0)
|
||||||
|
.cancelable(false)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val receiver = object : BroadcastReceiver() {
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
when (intent.getStringExtra(ACTION)) {
|
||||||
|
ACTION_BACKUP_COMPLETED_DIALOG -> {
|
||||||
|
backupDialog.dismiss()
|
||||||
|
val uri = Uri.parse(intent.getStringExtra(EXTRA_URI))
|
||||||
|
val file = UniFile.fromUri(context, uri)
|
||||||
|
MaterialDialog.Builder(this@SettingsBackupFragment.context)
|
||||||
|
.title(getString(R.string.backup_created))
|
||||||
|
.content(getString(R.string.file_saved, file.filePath))
|
||||||
|
.positiveText(getString(R.string.action_close))
|
||||||
|
.negativeText(getString(R.string.action_export))
|
||||||
|
.onPositive { materialDialog, _ -> materialDialog.dismiss() }
|
||||||
|
.onNegative { _, _ ->
|
||||||
|
val sendIntent = Intent(Intent.ACTION_SEND)
|
||||||
|
sendIntent.type = "application/json"
|
||||||
|
sendIntent.putExtra(Intent.EXTRA_STREAM, file.uri)
|
||||||
|
startActivity(Intent.createChooser(sendIntent, ""))
|
||||||
|
}
|
||||||
|
.safeShow()
|
||||||
|
|
||||||
|
}
|
||||||
|
ACTION_SET_PROGRESS_DIALOG -> {
|
||||||
|
val progress = intent.getIntExtra(EXTRA_PROGRESS, 0)
|
||||||
|
val amount = intent.getIntExtra(EXTRA_AMOUNT, 0)
|
||||||
|
val content = intent.getStringExtra(EXTRA_CONTENT)
|
||||||
|
restoreDialog.setContent(content)
|
||||||
|
restoreDialog.setProgress(progress)
|
||||||
|
restoreDialog.maxProgress = amount
|
||||||
|
}
|
||||||
|
ACTION_RESTORE_COMPLETED_DIALOG -> {
|
||||||
|
restoreDialog.dismiss()
|
||||||
|
val time = intent.getLongExtra(EXTRA_TIME, 0)
|
||||||
|
val errors = intent.getIntExtra(EXTRA_ERRORS, 0)
|
||||||
|
val path = intent.getStringExtra(EXTRA_ERROR_FILE_PATH)
|
||||||
|
val file = intent.getStringExtra(EXTRA_ERROR_FILE)
|
||||||
|
val timeString = String.format("%02d min, %02d sec",
|
||||||
|
TimeUnit.MILLISECONDS.toMinutes(time),
|
||||||
|
TimeUnit.MILLISECONDS.toSeconds(time) -
|
||||||
|
TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(time))
|
||||||
|
)
|
||||||
|
|
||||||
|
if (errors > 0) {
|
||||||
|
MaterialDialog.Builder(this@SettingsBackupFragment.context)
|
||||||
|
.title(getString(R.string.restore_completed))
|
||||||
|
.content(getString(R.string.restore_completed_content, timeString,
|
||||||
|
if (errors > 0) "$errors" else getString(android.R.string.no)))
|
||||||
|
.positiveText(getString(R.string.action_close))
|
||||||
|
.negativeText(getString(R.string.action_open_log))
|
||||||
|
.onPositive { materialDialog, _ -> materialDialog.dismiss() }
|
||||||
|
.onNegative { materialDialog, _ ->
|
||||||
|
if (!path.isEmpty()) {
|
||||||
|
val destFile = File(path, file)
|
||||||
|
val uri = destFile.getUriCompat(context)
|
||||||
|
val sendIntent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
setDataAndType(uri, "text/plain")
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
}
|
||||||
|
startActivity(sendIntent)
|
||||||
|
} else {
|
||||||
|
context.toast(getString(R.string.error_opening_log))
|
||||||
|
}
|
||||||
|
materialDialog.dismiss()
|
||||||
|
}
|
||||||
|
.safeShow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ACTION_ERROR_BACKUP_DIALOG -> {
|
||||||
|
context.toast(intent.getStringExtra(EXTRA_ERROR_MESSAGE))
|
||||||
|
backupDialog.dismiss()
|
||||||
|
}
|
||||||
|
ACTION_ERROR_RESTORE_DIALOG -> {
|
||||||
|
context.toast(intent.getStringExtra(EXTRA_ERROR_MESSAGE))
|
||||||
|
restoreDialog.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
context.registerLocalReceiver(receiver, IntentFilter(INTENT_FILTER))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
context.unregisterLocalReceiver(receiver)
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedState)
|
||||||
|
|
||||||
|
if (savedState != null) {
|
||||||
|
if (BackupRestoreService.isRunning(context)) {
|
||||||
|
restoreDialog.safeShow()
|
||||||
|
}
|
||||||
|
else if (BackupCreateService.isRunning(context)) {
|
||||||
|
backupDialog.safeShow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(activity as BaseActivity).requestPermissionsOnMarshmallow()
|
||||||
|
|
||||||
|
// Set onClickListeners
|
||||||
|
createBackup.setOnPreferenceClickListener {
|
||||||
|
MaterialDialog.Builder(context)
|
||||||
|
.title(R.string.pref_create_backup)
|
||||||
|
.content(R.string.backup_choice)
|
||||||
|
.items(R.array.backup_options)
|
||||||
|
.itemsCallbackMultiChoice(arrayOf(0, 1, 2, 3, 4 /*todo not hard code*/)) { _, positions, _ ->
|
||||||
|
// TODO not very happy with global value, but putExtra doesn't work
|
||||||
|
backup_flags = 0
|
||||||
|
for (i in 1..positions.size - 1) {
|
||||||
|
when (positions[i]) {
|
||||||
|
1 -> backup_flags = backup_flags or BackupCreateService.BACKUP_CATEGORY
|
||||||
|
2 -> backup_flags = backup_flags or BackupCreateService.BACKUP_CHAPTER
|
||||||
|
3 -> backup_flags = backup_flags or BackupCreateService.BACKUP_TRACK
|
||||||
|
4 -> backup_flags = backup_flags or BackupCreateService.BACKUP_HISTORY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If API lower as KitKat use custom dir picker
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
|
||||||
|
// Get dirs
|
||||||
|
val currentDir = preferences.backupsDirectory().getOrDefault()
|
||||||
|
|
||||||
|
val i = Intent(activity, CustomLayoutPickerActivity::class.java)
|
||||||
|
i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false)
|
||||||
|
i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true)
|
||||||
|
i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR)
|
||||||
|
i.putExtra(FilePickerActivity.EXTRA_START_PATH, currentDir)
|
||||||
|
startActivityForResult(i, BACKUP_CREATE)
|
||||||
|
} else {
|
||||||
|
// Use Androids build in file creator
|
||||||
|
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
|
||||||
|
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
|
||||||
|
// TODO create custom MIME data type? Will make older backups deprecated
|
||||||
|
intent.type = "application/*"
|
||||||
|
intent.putExtra(Intent.EXTRA_TITLE, Backup.getDefaultFilename())
|
||||||
|
startActivityForResult(intent, BACKUP_CREATE)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
.itemsDisabledIndices(0)
|
||||||
|
.positiveText(getString(R.string.action_create))
|
||||||
|
.negativeText(android.R.string.cancel)
|
||||||
|
.safeShow()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreBackup.setOnPreferenceClickListener {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
|
||||||
|
val intent = Intent()
|
||||||
|
intent.type = "application/*"
|
||||||
|
intent.action = Intent.ACTION_GET_CONTENT
|
||||||
|
startActivityForResult(Intent.createChooser(intent, getString(R.string.file_select_backup)), BACKUP_RESTORE)
|
||||||
|
} else {
|
||||||
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||||
|
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
intent.type = "application/*"
|
||||||
|
startActivityForResult(intent, BACKUP_RESTORE)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
automaticBackup.setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
// Always cancel the previous task, it seems that sometimes they are not updated.
|
||||||
|
BackupCreatorJob.cancelTask()
|
||||||
|
|
||||||
|
val interval = (newValue as String).toInt()
|
||||||
|
if (interval > 0) {
|
||||||
|
BackupCreatorJob.setupTask(interval)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
backupSlots.setOnPreferenceChangeListener { preference, newValue ->
|
||||||
|
preferences.numberOfBackups().set((newValue as String).toInt())
|
||||||
|
preference.summary = newValue
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
backupDirPref.setOnPreferenceClickListener {
|
||||||
|
val currentDir = preferences.backupsDirectory().getOrDefault()
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT < 21) {
|
||||||
|
// Custom dir selected, open directory selector
|
||||||
|
val i = Intent(activity, CustomLayoutPickerActivity::class.java)
|
||||||
|
i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false)
|
||||||
|
i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true)
|
||||||
|
i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR)
|
||||||
|
i.putExtra(FilePickerActivity.EXTRA_START_PATH, currentDir)
|
||||||
|
|
||||||
|
startActivityForResult(i, BACKUP_DIR)
|
||||||
|
} else {
|
||||||
|
val i = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||||
|
startActivityForResult(i, BACKUP_DIR)
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriptions += preferences.backupsDirectory().asObservable()
|
||||||
|
.subscribe { path ->
|
||||||
|
backupDir = UniFile.fromUri(context, Uri.parse(path))
|
||||||
|
backupDirPref.summary = backupDir.filePath ?: path
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriptions += preferences.backupInterval().asObservable()
|
||||||
|
.subscribe {
|
||||||
|
backupDirPref.isVisible = it > 0
|
||||||
|
backupSlots.isVisible = it > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
when (requestCode) {
|
||||||
|
BACKUP_DIR -> if (data != null && resultCode == Activity.RESULT_OK) {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
val uri = Uri.fromFile(File(data.data.path))
|
||||||
|
preferences.backupsDirectory().set(uri.toString())
|
||||||
|
} else {
|
||||||
|
val uri = data.data
|
||||||
|
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||||
|
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
|
|
||||||
|
context.contentResolver.takePersistableUriPermission(uri, flags)
|
||||||
|
|
||||||
|
val file = UniFile.fromUri(context, uri)
|
||||||
|
preferences.backupsDirectory().set(file.uri.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BACKUP_CREATE -> if (data != null && resultCode == Activity.RESULT_OK) {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
|
||||||
|
val dir = data.data.path
|
||||||
|
val file = File(dir, Backup.getDefaultFilename())
|
||||||
|
|
||||||
|
backupDialog.safeShow()
|
||||||
|
BackupCreateService.makeBackup(context, file.toURI().toString(), backup_flags)
|
||||||
|
} else {
|
||||||
|
val uri = data.data
|
||||||
|
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||||
|
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
|
|
||||||
|
context.contentResolver.takePersistableUriPermission(uri, flags)
|
||||||
|
val file = UniFile.fromUri(context, uri)
|
||||||
|
|
||||||
|
backupDialog.safeShow()
|
||||||
|
BackupCreateService.makeBackup(context, file.uri.toString(), backup_flags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BACKUP_RESTORE -> if (data != null && resultCode == Activity.RESULT_OK) {
|
||||||
|
val uri = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
|
||||||
|
Uri.fromFile(File(data.data.path))
|
||||||
|
} else {
|
||||||
|
val uri = data.data
|
||||||
|
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||||
|
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
|
|
||||||
|
context.contentResolver.takePersistableUriPermission(uri, flags)
|
||||||
|
UniFile.fromUri(context, uri).uri
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialDialog.Builder(context)
|
||||||
|
.title(getString(R.string.pref_restore_backup))
|
||||||
|
.content(getString(R.string.backup_restore_content))
|
||||||
|
.positiveText(getString(R.string.action_restore))
|
||||||
|
.onPositive { _, _ ->
|
||||||
|
restoreDialog.safeShow()
|
||||||
|
BackupRestoreService.start(context, uri.toString())
|
||||||
|
}
|
||||||
|
.safeShow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun MaterialDialog.Builder.safeShow(): Dialog {
|
||||||
|
return build().safeShow()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Dialog.safeShow(): Dialog {
|
||||||
|
subscriptions += Subscriptions.create { dismiss() }
|
||||||
|
show()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -9,20 +9,17 @@ 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.Preference
|
||||||
import android.support.v7.preference.XpPreferenceFragment
|
import android.support.v7.preference.XpPreferenceFragment
|
||||||
import android.support.v7.widget.RecyclerView
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import com.nononsenseapps.filepicker.AbstractFilePickerFragment
|
|
||||||
import com.nononsenseapps.filepicker.FilePickerActivity
|
import com.nononsenseapps.filepicker.FilePickerActivity
|
||||||
import com.nononsenseapps.filepicker.FilePickerFragment
|
|
||||||
import com.nononsenseapps.filepicker.LogicHandler
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
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.util.inflate
|
|
||||||
import eu.kanade.tachiyomi.util.plusAssign
|
import eu.kanade.tachiyomi.util.plusAssign
|
||||||
|
import eu.kanade.tachiyomi.widget.CustomLayoutPickerActivity
|
||||||
|
import net.xpece.android.support.preference.MultiSelectListPreference
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@ -41,8 +38,12 @@ class SettingsDownloadsFragment : SettingsFragment() {
|
|||||||
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
private val db: DatabaseHelper by injectLazy()
|
||||||
|
|
||||||
val downloadDirPref: Preference by bindPref(R.string.pref_download_directory_key)
|
val downloadDirPref: Preference by bindPref(R.string.pref_download_directory_key)
|
||||||
|
|
||||||
|
val downloadCategory: MultiSelectListPreference by bindPref(R.string.pref_download_new_categories_key)
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedState: Bundle?) {
|
override fun onViewCreated(view: View, savedState: Bundle?) {
|
||||||
super.onViewCreated(view, savedState)
|
super.onViewCreated(view, savedState)
|
||||||
|
|
||||||
@ -92,6 +93,29 @@ class SettingsDownloadsFragment : SettingsFragment() {
|
|||||||
dir.createFile(".nomedia")
|
dir.createFile(".nomedia")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
subscriptions += preferences.downloadNew().asObservable()
|
||||||
|
.subscribe { downloadCategory.isVisible = it }
|
||||||
|
|
||||||
|
val dbCategories = db.getCategories().executeAsBlocking()
|
||||||
|
downloadCategory.apply {
|
||||||
|
entries = dbCategories.map { it.name }.toTypedArray()
|
||||||
|
entryValues = dbCategories.map { it.id.toString() }.toTypedArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriptions += preferences.downloadNewCategories().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 }
|
||||||
|
|
||||||
|
downloadCategory.summary = summary
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getExternalFilesDirs(): List<File> {
|
fun getExternalFilesDirs(): List<File> {
|
||||||
@ -122,27 +146,4 @@ class SettingsDownloadsFragment : SettingsFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CustomLayoutPickerActivity : FilePickerActivity() {
|
|
||||||
|
|
||||||
override fun getFragment(startPath: String?, mode: Int, allowMultiple: Boolean, allowCreateDir: Boolean):
|
|
||||||
AbstractFilePickerFragment<File> {
|
|
||||||
val fragment = CustomLayoutFilePickerFragment()
|
|
||||||
fragment.setArgs(startPath, mode, allowMultiple, allowCreateDir)
|
|
||||||
return fragment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class CustomLayoutFilePickerFragment : FilePickerFragment() {
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
|
||||||
when (viewType) {
|
|
||||||
LogicHandler.VIEWTYPE_DIR -> {
|
|
||||||
val view = parent.inflate(R.layout.listitem_dir)
|
|
||||||
return DirViewHolder(view)
|
|
||||||
}
|
|
||||||
else -> return super.onCreateViewHolder(parent, viewType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,7 @@ open class SettingsFragment : XpPreferenceFragment() {
|
|||||||
addPreferencesFromResource(R.xml.pref_downloads)
|
addPreferencesFromResource(R.xml.pref_downloads)
|
||||||
addPreferencesFromResource(R.xml.pref_sources)
|
addPreferencesFromResource(R.xml.pref_sources)
|
||||||
addPreferencesFromResource(R.xml.pref_tracking)
|
addPreferencesFromResource(R.xml.pref_tracking)
|
||||||
|
addPreferencesFromResource(R.xml.pref_backup)
|
||||||
addPreferencesFromResource(R.xml.pref_advanced)
|
addPreferencesFromResource(R.xml.pref_advanced)
|
||||||
addPreferencesFromResource(R.xml.pref_about)
|
addPreferencesFromResource(R.xml.pref_about)
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ 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.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.util.LocaleHelper
|
import eu.kanade.tachiyomi.util.LocaleHelper
|
||||||
@ -46,6 +47,8 @@ class SettingsGeneralFragment : SettingsFragment(),
|
|||||||
|
|
||||||
val categoryUpdate: MultiSelectListPreference by bindPref(R.string.pref_library_update_categories_key)
|
val categoryUpdate: MultiSelectListPreference by bindPref(R.string.pref_library_update_categories_key)
|
||||||
|
|
||||||
|
val defaultCategory: IntListPreference by bindPref(R.string.default_category_key)
|
||||||
|
|
||||||
val langPreference: ListPreference by bindPref(R.string.pref_language_key)
|
val langPreference: ListPreference by bindPref(R.string.pref_language_key)
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedState: Bundle?) {
|
override fun onViewCreated(view: View, savedState: Bundle?) {
|
||||||
@ -100,6 +103,22 @@ class SettingsGeneralFragment : SettingsFragment(),
|
|||||||
categoryUpdate.summary = summary
|
categoryUpdate.summary = summary
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defaultCategory.apply {
|
||||||
|
val selectedCategory = dbCategories.find { it.id == preferences.defaultCategory()}
|
||||||
|
value = selectedCategory?.id?.toString() ?: value
|
||||||
|
entries += dbCategories.map { it.name }.toTypedArray()
|
||||||
|
entryValues += dbCategories.map { it.id.toString() }.toTypedArray()
|
||||||
|
summary = selectedCategory?.name ?: summary
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultCategory.setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
defaultCategory.summary = dbCategories.find {
|
||||||
|
it.id == (newValue as String).toInt()
|
||||||
|
}?.name ?: getString(R.string.default_category_summary)
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
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()
|
||||||
|
@ -122,7 +122,7 @@ fun Context.sendLocalBroadcastSync(intent: Intent) {
|
|||||||
*
|
*
|
||||||
* @param receiver receiver that gets registered.
|
* @param receiver receiver that gets registered.
|
||||||
*/
|
*/
|
||||||
fun Context.registerLocalReceiver(receiver: BroadcastReceiver, filter: IntentFilter ){
|
fun Context.registerLocalReceiver(receiver: BroadcastReceiver, filter: IntentFilter) {
|
||||||
LocalBroadcastManager.getInstance(this).registerReceiver(receiver, filter)
|
LocalBroadcastManager.getInstance(this).registerReceiver(receiver, filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,7 +131,7 @@ fun Context.registerLocalReceiver(receiver: BroadcastReceiver, filter: IntentFil
|
|||||||
*
|
*
|
||||||
* @param receiver receiver that gets unregistered.
|
* @param receiver receiver that gets unregistered.
|
||||||
*/
|
*/
|
||||||
fun Context.unregisterLocalReceiver(receiver: BroadcastReceiver){
|
fun Context.unregisterLocalReceiver(receiver: BroadcastReceiver) {
|
||||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver)
|
LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
package eu.kanade.tachiyomi.util
|
package eu.kanade.tachiyomi.util
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.support.v4.content.ContextCompat
|
import android.support.v4.content.ContextCompat
|
||||||
import android.support.v4.os.EnvironmentCompat
|
import android.support.v4.os.EnvironmentCompat
|
||||||
@ -13,8 +16,11 @@ import java.security.NoSuchAlgorithmException
|
|||||||
object DiskUtil {
|
object DiskUtil {
|
||||||
|
|
||||||
fun isImage(name: String, openStream: (() -> InputStream)? = null): Boolean {
|
fun isImage(name: String, openStream: (() -> InputStream)? = null): Boolean {
|
||||||
val contentType = URLConnection.guessContentTypeFromName(name)
|
val contentType = try {
|
||||||
?: openStream?.let { findImageMime(it) }
|
URLConnection.guessContentTypeFromName(name)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
} ?: openStream?.let { findImageMime(it) }
|
||||||
|
|
||||||
return contentType?.startsWith("image/") ?: false
|
return contentType?.startsWith("image/") ?: false
|
||||||
}
|
}
|
||||||
@ -73,8 +79,9 @@ object DiskUtil {
|
|||||||
/**
|
/**
|
||||||
* Returns the root folders of all the available external storages.
|
* Returns the root folders of all the available external storages.
|
||||||
*/
|
*/
|
||||||
fun getExternalStorages(context: Context): List<File> {
|
fun getExternalStorages(context: Context): Collection<File> {
|
||||||
return ContextCompat.getExternalFilesDirs(context, null)
|
val directories = mutableSetOf<File>()
|
||||||
|
directories += ContextCompat.getExternalFilesDirs(context, null)
|
||||||
.filterNotNull()
|
.filterNotNull()
|
||||||
.mapNotNull {
|
.mapNotNull {
|
||||||
val file = File(it.absolutePath.substringBefore("/Android/"))
|
val file = File(it.absolutePath.substringBefore("/Android/"))
|
||||||
@ -85,6 +92,29 @@ object DiskUtil {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT < 21) {
|
||||||
|
val extStorages = System.getenv("SECONDARY_STORAGE")
|
||||||
|
if (extStorages != null) {
|
||||||
|
directories += extStorages.split(":").map(::File)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return directories
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scans the given file so that it can be shown in gallery apps, for example.
|
||||||
|
*/
|
||||||
|
fun scanMedia(context: Context, file: File) {
|
||||||
|
val action = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
|
||||||
|
Intent.ACTION_MEDIA_MOUNTED
|
||||||
|
} else {
|
||||||
|
Intent.ACTION_MEDIA_SCANNER_SCAN_FILE
|
||||||
|
}
|
||||||
|
val mediaScanIntent = Intent(action)
|
||||||
|
mediaScanIntent.data = Uri.fromFile(file)
|
||||||
|
context.sendBroadcast(mediaScanIntent)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -19,15 +19,3 @@ fun File.getUriCompat(context: Context): Uri {
|
|||||||
return uri
|
return uri
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes file if exists
|
|
||||||
*
|
|
||||||
* @return success of file deletion
|
|
||||||
*/
|
|
||||||
fun File.deleteIfExists(): Boolean {
|
|
||||||
if (this.exists()) {
|
|
||||||
this.delete()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
package eu.kanade.tachiyomi.widget
|
||||||
|
|
||||||
|
import android.support.v7.widget.RecyclerView
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import com.nononsenseapps.filepicker.AbstractFilePickerFragment
|
||||||
|
import com.nononsenseapps.filepicker.FilePickerActivity
|
||||||
|
import com.nononsenseapps.filepicker.FilePickerFragment
|
||||||
|
import com.nononsenseapps.filepicker.LogicHandler
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.util.inflate
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class CustomLayoutPickerActivity : FilePickerActivity() {
|
||||||
|
|
||||||
|
override fun getFragment(startPath: String?, mode: Int, allowMultiple: Boolean, allowCreateDir: Boolean):
|
||||||
|
AbstractFilePickerFragment<File> {
|
||||||
|
val fragment = CustomLayoutFilePickerFragment()
|
||||||
|
fragment.setArgs(startPath, mode, allowMultiple, allowCreateDir)
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomLayoutFilePickerFragment : FilePickerFragment() {
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
|
when (viewType) {
|
||||||
|
LogicHandler.VIEWTYPE_DIR -> {
|
||||||
|
val view = parent.inflate(R.layout.listitem_dir)
|
||||||
|
return DirViewHolder(view)
|
||||||
|
}
|
||||||
|
else -> return super.onCreateViewHolder(parent, viewType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Before Width: | Height: | Size: 7.8 KiB |
BIN
app/src/main/res/drawable-hdpi/ic_book_white_24dp.png
Normal file
After Width: | Height: | Size: 180 B |
BIN
app/src/main/res/drawable-hdpi/tachiyomi_circle.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 4.5 KiB |
BIN
app/src/main/res/drawable-mdpi/ic_book_white_24dp.png
Normal file
After Width: | Height: | Size: 136 B |
BIN
app/src/main/res/drawable-mdpi/tachiyomi_circle.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 12 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_book_white_24dp.png
Normal file
After Width: | Height: | Size: 197 B |
BIN
app/src/main/res/drawable-xhdpi/tachiyomi_circle.png
Normal file
After Width: | Height: | Size: 7.0 KiB |
Before Width: | Height: | Size: 22 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_book_white_24dp.png
Normal file
After Width: | Height: | Size: 283 B |
BIN
app/src/main/res/drawable-xxhdpi/tachiyomi_circle.png
Normal file
After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 25 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/ic_book_white_24dp.png
Normal file
After Width: | Height: | Size: 350 B |
BIN
app/src/main/res/drawable-xxxhdpi/tachiyomi_circle.png
Normal file
After Width: | Height: | Size: 16 KiB |
@ -1,10 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:opacity="opaque">
|
android:opacity="opaque">
|
||||||
<item android:drawable="@color/background_material_light"/>
|
<item android:drawable="@color/material_grey_50"/>
|
||||||
<item>
|
<item>
|
||||||
<bitmap
|
<bitmap
|
||||||
android:src="@drawable/application_logo_144dp"
|
android:src="@drawable/branded_logo_icon"
|
||||||
android:gravity="center"/>
|
android:gravity="center"/>
|
||||||
</item>
|
</item>
|
||||||
</layer-list>
|
</layer-list>
|
BIN
app/src/main/res/drawable/branded_logo_icon.png
Normal file
After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 16 KiB |
19
app/src/main/res/drawable/sc_book_48dp.xml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="48dp"
|
||||||
|
android:height="48dp"
|
||||||
|
android:viewportHeight="48"
|
||||||
|
android:viewportWidth="48">
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/backgroundLight"
|
||||||
|
android:pathData="M24,24m-22,0a22,22 0,1 1,44 0a22,22 0,1 1,-44 0" />
|
||||||
|
|
||||||
|
<group
|
||||||
|
android:translateX="12"
|
||||||
|
android:translateY="12">
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/colorPrimary"
|
||||||
|
android:pathData="M18,2H6c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V4c0,-1.1 -0.9,-2 -2,-2zM6,4h5v8l-2.5,-1.5L6,12V4z" />
|
||||||
|
</group>
|
||||||
|
</vector>
|
19
app/src/main/res/drawable/sc_explore_48dp.xml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="48dp"
|
||||||
|
android:height="48dp"
|
||||||
|
android:viewportHeight="48"
|
||||||
|
android:viewportWidth="48">
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/backgroundLight"
|
||||||
|
android:pathData="M24,24m-22,0a22,22 0,1 1,44 0a22,22 0,1 1,-44 0" />
|
||||||
|
|
||||||
|
<group
|
||||||
|
android:translateX="12"
|
||||||
|
android:translateY="12">
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/colorPrimary"
|
||||||
|
android:pathData="M12,10.9c-0.61,0 -1.1,0.49 -1.1,1.1s0.49,1.1 1.1,1.1c0.61,0 1.1,-0.49 1.1,-1.1s-0.49,-1.1 -1.1,-1.1zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM14.19,14.19L6,18l3.81,-8.19L18,6l-3.81,8.19z" />
|
||||||
|
</group>
|
||||||
|
</vector>
|
19
app/src/main/res/drawable/sc_glasses_48dp.xml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="48dp"
|
||||||
|
android:height="48dp"
|
||||||
|
android:viewportHeight="48"
|
||||||
|
android:viewportWidth="48">
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/backgroundLight"
|
||||||
|
android:pathData="M24,24m-22,0a22,22 0,1 1,44 0a22,22 0,1 1,-44 0" />
|
||||||
|
|
||||||
|
<group
|
||||||
|
android:translateX="12"
|
||||||
|
android:translateY="12">
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/colorPrimary"
|
||||||
|
android:pathData="M3,10C2.76,10 2.55,10.09 2.41,10.25C2.27,10.4 2.21,10.62 2.24,10.86L2.74,13.85C2.82,14.5 3.4,15 4,15H7C7.64,15 8.36,14.44 8.5,13.82L9.56,10.63C9.6,10.5 9.57,10.31 9.5,10.19C9.39,10.07 9.22,10 9,10H3M7,17H4C2.38,17 0.96,15.74 0.76,14.14L0.26,11.15C0.15,10.3 0.39,9.5 0.91,8.92C1.43,8.34 2.19,8 3,8H9C9.83,8 10.58,8.35 11.06,8.96C11.17,9.11 11.27,9.27 11.35,9.45C11.78,9.36 12.22,9.36 12.64,9.45C12.72,9.27 12.82,9.11 12.94,8.96C13.41,8.35 14.16,8 15,8H21C21.81,8 22.57,8.34 23.09,8.92C23.6,9.5 23.84,10.3 23.74,11.11L23.23,14.18C23.04,15.74 21.61,17 20,17H17C15.44,17 13.92,15.81 13.54,14.3L12.64,11.59C12.26,11.31 11.73,11.31 11.35,11.59L10.43,14.37C10.07,15.82 8.56,17 7,17M15,10C14.78,10 14.61,10.07 14.5,10.19C14.42,10.31 14.4,10.5 14.45,10.7L15.46,13.75C15.64,14.44 16.36,15 17,15H20C20.59,15 21.18,14.5 21.25,13.89L21.76,10.82C21.79,10.62 21.73,10.4 21.59,10.25C21.45,10.09 21.24,10 21,10H15Z" />
|
||||||
|
</group>
|
||||||
|
</vector>
|
19
app/src/main/res/drawable/sc_update_48dp.xml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="48dp"
|
||||||
|
android:height="48dp"
|
||||||
|
android:viewportHeight="48"
|
||||||
|
android:viewportWidth="48">
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/backgroundLight"
|
||||||
|
android:pathData="M24,24m-22,0a22,22 0,1 1,44 0a22,22 0,1 1,-44 0" />
|
||||||
|
|
||||||
|
<group
|
||||||
|
android:translateX="12"
|
||||||
|
android:translateY="12">
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/colorPrimary"
|
||||||
|
android:pathData="M21,10.12h-6.78l2.74,-2.82c-2.73,-2.7 -7.15,-2.8 -9.88,-0.1 -2.73,2.71 -2.73,7.08 0,9.79 2.73,2.71 7.15,2.71 9.88,0C18.32,15.65 19,14.08 19,12.1h2c0,1.98 -0.88,4.55 -2.64,6.29 -3.51,3.48 -9.21,3.48 -12.72,0 -3.5,-3.47 -3.53,-9.11 -0.02,-12.58 3.51,-3.47 9.14,-3.47 12.65,0L21,3v7.12zM12.5,8v4.25l3.5,2.08 -0.72,1.21L11,13V8h1.5z" />
|
||||||
|
</group>
|
||||||
|
</vector>
|
@ -13,8 +13,7 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="?attr/colorAccent"
|
android:background="?attr/colorAccent"
|
||||||
android:elevation="5dp"
|
android:elevation="5dp"
|
||||||
android:visibility="invisible"
|
android:visibility="invisible"/>
|
||||||
/>
|
|
||||||
|
|
||||||
<android.support.v4.widget.SwipeRefreshLayout
|
<android.support.v4.widget.SwipeRefreshLayout
|
||||||
android:id="@+id/swipe_refresh"
|
android:id="@+id/swipe_refresh"
|
||||||
@ -36,6 +35,17 @@
|
|||||||
|
|
||||||
</android.support.v4.widget.SwipeRefreshLayout>
|
</android.support.v4.widget.SwipeRefreshLayout>
|
||||||
|
|
||||||
|
<eu.davidea.fastscroller.FastScroller
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/fast_scroller"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:layout_gravity="end"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible"/>
|
||||||
|
|
||||||
<android.support.design.widget.FloatingActionButton
|
<android.support.design.widget.FloatingActionButton
|
||||||
android:id="@+id/fab"
|
android:id="@+id/fab"
|
||||||
style="@style/Theme.Widget.FAB"
|
style="@style/Theme.Widget.FAB"
|
||||||
|
@ -1,298 +1,266 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<android.support.design.widget.CoordinatorLayout
|
<android.support.v4.widget.SwipeRefreshLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
tools:context="eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment"
|
||||||
|
android:id="@id/swipe_refresh"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent">
|
||||||
tools:context="eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment">
|
|
||||||
|
|
||||||
<android.support.v4.widget.SwipeRefreshLayout
|
<android.support.constraint.ConstraintLayout
|
||||||
android:id="@id/swipe_refresh"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent"
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<LinearLayout
|
<android.support.constraint.Guideline
|
||||||
android:id="@+id/global_view"
|
android:layout_width="wrap_content"
|
||||||
android:layout_width="match_parent"
|
android:layout_height="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:id="@+id/guideline"
|
||||||
android:orientation="vertical">
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintGuide_percent="0.38"/>
|
||||||
|
|
||||||
<RelativeLayout
|
<android.support.constraint.Guideline
|
||||||
android:id="@+id/top_view"
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:id="@+id/guideline2"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layout_constraintGuide_percent="0.38"/>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/backdrop"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:alpha="0.2"
|
||||||
|
tools:background="@color/material_grey_700"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/guideline"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"/>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/manga_cover"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:contentDescription="@string/description_cover"
|
||||||
|
tools:background="@color/material_grey_700"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:layout_marginLeft="16dp"
|
||||||
|
android:layout_marginRight="16dp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/guideline"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toLeftOf="@+id/guideline2"/>
|
||||||
|
|
||||||
|
<android.support.design.widget.FloatingActionButton
|
||||||
|
android:id="@+id/fab_favorite"
|
||||||
|
style="@style/Theme.Widget.FAB"
|
||||||
|
app:srcCompat="@drawable/ic_bookmark_border_white_24dp"
|
||||||
|
android:layout_marginTop="0dp"
|
||||||
|
android:layout_marginBottom="0dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/guideline"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/guideline"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"/>
|
||||||
|
|
||||||
|
<android.support.v4.widget.NestedScrollView
|
||||||
|
android:id="@+id/info_scrollview"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:layout_marginLeft="0dp"
|
||||||
|
android:layout_marginRight="16dp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/guideline"
|
||||||
|
app:layout_constraintLeft_toLeftOf="@+id/guideline2"
|
||||||
|
app:layout_constraintRight_toRightOf="parent">
|
||||||
|
|
||||||
|
<android.support.constraint.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="match_parent">
|
||||||
android:layout_weight="0.4">
|
|
||||||
|
|
||||||
<ImageView
|
<TextView
|
||||||
android:id="@+id/backdrop"
|
android:id="@+id/manga_author_label"
|
||||||
android:layout_width="match_parent"
|
style="@style/TextAppearance.Medium.Body2"
|
||||||
android:layout_height="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:alpha="0.2"
|
android:layout_height="wrap_content"
|
||||||
android:contentDescription="@string/description_backdrop"/>
|
android:text="@string/manga_info_author_label"
|
||||||
|
android:textIsSelectable="false"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"/>
|
||||||
|
|
||||||
<LinearLayout
|
<TextView
|
||||||
android:layout_width="match_parent"
|
android:id="@+id/manga_author"
|
||||||
android:layout_height="match_parent"
|
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||||
android:orientation="horizontal">
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textIsSelectable="false"
|
||||||
|
app:layout_constraintBaseline_toBaselineOf="@+id/manga_author_label"
|
||||||
|
app:layout_constraintLeft_toRightOf="@+id/manga_author_label"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"/>
|
||||||
|
|
||||||
<ImageView
|
<TextView
|
||||||
android:id="@+id/manga_cover"
|
android:id="@+id/manga_artist_label"
|
||||||
android:layout_width="0dp"
|
style="@style/TextAppearance.Medium.Body2"
|
||||||
android:layout_height="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_margin="@dimen/activity_vertical_margin"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="0.35"
|
android:text="@string/manga_info_artist_label"
|
||||||
android:contentDescription="@string/description_cover"/>
|
android:textIsSelectable="false"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/manga_author_label"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"/>
|
||||||
|
|
||||||
<RelativeLayout
|
<TextView
|
||||||
android:layout_width="0dp"
|
android:id="@+id/manga_artist"
|
||||||
android:layout_height="match_parent"
|
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||||
android:layout_margin="@dimen/activity_vertical_margin"
|
android:layout_width="0dp"
|
||||||
android:layout_weight="0.65">
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textIsSelectable="false"
|
||||||
|
app:layout_constraintBaseline_toBaselineOf="@+id/manga_artist_label"
|
||||||
|
app:layout_constraintLeft_toRightOf="@+id/manga_artist_label"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/manga_chapters_label"
|
||||||
|
style="@style/TextAppearance.Medium.Body2"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/manga_info_chapters_label"
|
||||||
|
android:textIsSelectable="false"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/manga_artist_label"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"/>
|
||||||
|
|
||||||
<android.support.v4.widget.NestedScrollView
|
<TextView
|
||||||
android:layout_width="match_parent"
|
android:id="@+id/manga_chapters"
|
||||||
android:layout_height="match_parent">
|
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textIsSelectable="false"
|
||||||
|
app:layout_constraintBaseline_toBaselineOf="@+id/manga_chapters_label"
|
||||||
|
app:layout_constraintLeft_toRightOf="@+id/manga_chapters_label"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"/>
|
||||||
|
|
||||||
<RelativeLayout
|
<TextView
|
||||||
android:layout_width="match_parent"
|
android:id="@+id/manga_status_label"
|
||||||
android:layout_height="match_parent">
|
style="@style/TextAppearance.Medium.Body2"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/manga_info_status_label"
|
||||||
|
android:textIsSelectable="false"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/manga_chapters_label"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"/>
|
||||||
|
|
||||||
<LinearLayout
|
<TextView
|
||||||
android:id="@+id/manga_author_view"
|
android:id="@+id/manga_status"
|
||||||
android:layout_width="match_parent"
|
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||||
android:layout_height="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:orientation="horizontal">
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textIsSelectable="false"
|
||||||
|
app:layout_constraintBaseline_toBaselineOf="@+id/manga_status_label"
|
||||||
|
app:layout_constraintLeft_toRightOf="@+id/manga_status_label"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/manga_author_label"
|
android:id="@+id/manga_source_label"
|
||||||
style="@style/TextAppearance.Medium.Body2"
|
style="@style/TextAppearance.Medium.Body2"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:ellipsize="end"
|
android:text="@string/manga_info_source_label"
|
||||||
android:paddingRight="10dp"
|
android:textIsSelectable="false"
|
||||||
android:singleLine="true"
|
app:layout_constraintTop_toBottomOf="@+id/manga_status_label"
|
||||||
android:text="@string/manga_info_author_label"
|
app:layout_constraintLeft_toLeftOf="parent"/>
|
||||||
android:textIsSelectable="false"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/manga_author"
|
android:id="@+id/manga_source"
|
||||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:ellipsize="end"
|
android:layout_marginLeft="8dp"
|
||||||
android:singleLine="true"
|
android:ellipsize="end"
|
||||||
android:textIsSelectable="false"
|
android:maxLines="1"
|
||||||
/>
|
android:textIsSelectable="false"
|
||||||
</LinearLayout>
|
app:layout_constraintBaseline_toBaselineOf="@+id/manga_source_label"
|
||||||
|
app:layout_constraintLeft_toRightOf="@+id/manga_source_label"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"/>
|
||||||
|
|
||||||
<LinearLayout
|
<TextView
|
||||||
android:id="@+id/manga_artist_view"
|
android:id="@+id/manga_genres_label"
|
||||||
android:layout_width="match_parent"
|
style="@style/TextAppearance.Medium.Body2"
|
||||||
android:layout_height="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_below="@+id/manga_author_view"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal">
|
android:text="@string/manga_info_genres_label"
|
||||||
|
android:textIsSelectable="false"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/manga_source_label"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/manga_artist_label"
|
android:id="@+id/manga_genres"
|
||||||
style="@style/TextAppearance.Medium.Body2"
|
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:ellipsize="end"
|
android:textIsSelectable="false"
|
||||||
android:paddingRight="10dp"
|
app:layout_constraintTop_toBottomOf="@+id/manga_genres_label"
|
||||||
android:singleLine="true"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
android:text="@string/manga_info_artist_label"
|
app:layout_constraintRight_toRightOf="parent"/>
|
||||||
android:textIsSelectable="false"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextView
|
</android.support.constraint.ConstraintLayout>
|
||||||
android:id="@+id/manga_artist"
|
|
||||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:textIsSelectable="false"
|
|
||||||
/>
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
</android.support.v4.widget.NestedScrollView>
|
||||||
android:id="@+id/manga_chapters_view"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_below="@+id/manga_artist_view"
|
|
||||||
android:orientation="horizontal">
|
|
||||||
|
|
||||||
<TextView
|
<android.support.v4.widget.NestedScrollView
|
||||||
android:id="@+id/manga_chapters_label"
|
android:id="@+id/description_scrollview"
|
||||||
style="@style/TextAppearance.Medium.Body2"
|
android:layout_width="0dp"
|
||||||
android:layout_width="wrap_content"
|
android:layout_height="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_marginBottom="16dp"
|
||||||
android:ellipsize="end"
|
android:layout_marginEnd="8dp"
|
||||||
android:paddingRight="10dp"
|
android:layout_marginLeft="16dp"
|
||||||
android:singleLine="true"
|
android:layout_marginRight="16dp"
|
||||||
android:text="@string/manga_info_chapters_label"
|
android:layout_marginStart="8dp"
|
||||||
android:textIsSelectable="false"
|
android:layout_marginTop="16dp"
|
||||||
/>
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/guideline">
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/manga_chapters"
|
|
||||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:textIsSelectable="false"
|
|
||||||
/>
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/manga_status_view"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_below="@+id/manga_chapters_view"
|
|
||||||
android:orientation="horizontal">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/manga_status_label"
|
|
||||||
style="@style/TextAppearance.Medium.Body2"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:paddingRight="10dp"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:text="@string/manga_info_status_label"
|
|
||||||
android:textIsSelectable="false"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/manga_status"
|
|
||||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:textIsSelectable="false"
|
|
||||||
/>
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/manga_source_view"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_below="@+id/manga_status_view"
|
|
||||||
android:orientation="horizontal">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/manga_source_label"
|
|
||||||
style="@style/TextAppearance.Medium.Body2"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:paddingRight="10dp"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:text="@string/manga_info_source_label"
|
|
||||||
android:textIsSelectable="false"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/manga_source"
|
|
||||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:textIsSelectable="false"
|
|
||||||
/>
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/manga_genres_view"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_below="@+id/manga_source_view"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/manga_genres_label"
|
|
||||||
style="@style/TextAppearance.Medium.Body2"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:paddingRight="10dp"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:text="@string/manga_info_genres_label"
|
|
||||||
android:textIsSelectable="false"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/manga_genres"
|
|
||||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:singleLine="false"
|
|
||||||
android:textIsSelectable="false"
|
|
||||||
/>
|
|
||||||
</LinearLayout>
|
|
||||||
</RelativeLayout>
|
|
||||||
</android.support.v4.widget.NestedScrollView>
|
|
||||||
</RelativeLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
||||||
|
|
||||||
<android.support.v4.widget.NestedScrollView
|
|
||||||
android:id="@+id/bottom_view"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="@dimen/activity_vertical_margin"
|
android:orientation="vertical">
|
||||||
android:layout_weight="0.6">
|
|
||||||
|
|
||||||
<LinearLayout
|
<TextView
|
||||||
|
android:id="@+id/manga_summary_label"
|
||||||
|
style="@style/TextAppearance.Medium.Body2"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical">
|
android:text="@string/description"
|
||||||
|
android:textIsSelectable="false"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/manga_summary_label"
|
android:id="@+id/manga_summary"
|
||||||
style="@style/TextAppearance.Medium.Body2"
|
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:ellipsize="end"
|
android:textIsSelectable="false"/>
|
||||||
android:paddingRight="10dp"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:text="@string/description"
|
|
||||||
android:textIsSelectable="false"/>
|
|
||||||
|
|
||||||
<TextView
|
</LinearLayout>
|
||||||
android:id="@+id/manga_summary"
|
|
||||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:singleLine="false"
|
|
||||||
android:textIsSelectable="false"
|
|
||||||
/>
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</android.support.v4.widget.NestedScrollView>
|
</android.support.v4.widget.NestedScrollView>
|
||||||
|
|
||||||
|
</android.support.constraint.ConstraintLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</android.support.v4.widget.SwipeRefreshLayout>
|
||||||
|
|
||||||
</android.support.v4.widget.SwipeRefreshLayout>
|
|
||||||
|
|
||||||
<android.support.design.widget.FloatingActionButton
|
|
||||||
android:id="@+id/fab_favorite"
|
|
||||||
style="@style/Theme.Widget.FAB"
|
|
||||||
android:layout_gravity=""
|
|
||||||
app:layout_anchor="@id/top_view"
|
|
||||||
app:layout_anchorGravity="bottom|right|end"
|
|
||||||
app:layout_behavior=""
|
|
||||||
app:srcCompat="@drawable/ic_bookmark_border_white_24dp"/>
|
|
||||||
|
|
||||||
</android.support.design.widget.CoordinatorLayout>
|
|
||||||
|
@ -17,6 +17,6 @@
|
|||||||
android:layout_marginLeft="16dp"
|
android:layout_marginLeft="16dp"
|
||||||
android:layout_marginTop="@dimen/navigation_drawer_header_margin"
|
android:layout_marginTop="@dimen/navigation_drawer_header_margin"
|
||||||
android:scaleType="centerCrop"
|
android:scaleType="centerCrop"
|
||||||
android:src="@drawable/icon"/>
|
android:src="@drawable/tachiyomi_circle"/>
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|