Compare commits

..

31 Commits

Author SHA1 Message Date
len
0cffb9e503 Fix F-Droid build 2016-05-01 18:25:40 +02:00
len
e2ecf0ce5f Release 0.2.1 2016-04-27 14:06:45 +02:00
len
5d396bfb7c Make query non nullable, it fixes some bugs in the catalogue 2016-04-27 00:58:05 +02:00
len
de6cc8394e Fixed some crashes in the catalogue and the reader 2016-04-27 00:23:06 +02:00
4b7159648a Merge pull request #257 from NoodleMage/update_improv
Rewrote UpdateDownloader to Kotlin + AC fixes
2016-04-27 00:20:50 +02:00
eb9c5f95db Removed automatic update + duplicate fix 2016-04-26 22:56:49 +02:00
55e9d2880c Rewrote UpdateDownloader to Kotlin
Added auto update check (every 12 hour)
Warning message optional fix #256
Lots of bug fixes!
2016-04-26 20:57:05 +02:00
len
ec9c19ce7d Use a thread-safe list for downloads 2016-04-26 14:00:22 +02:00
len
31731e8f26 Fix a crash in older android versions 2016-04-25 23:43:32 +02:00
len
bfb12bc7c1 Minor changes to fix a possible crash in the downloads view 2016-04-24 23:32:49 +02:00
len
4befcf3819 Fix #277, library not updating 2016-04-23 15:39:41 +02:00
cb58145361 Allow setting versionCode in parameter (#276)
Allow easier debug versionCode change
2016-04-23 13:17:48 +02:00
len
b83efd90a8 Slightly increase library view performance by caching typefaces 2016-04-22 19:41:59 +02:00
len
9f0da3f1d6 Upgrade to nucleus 3 2016-04-22 14:28:06 +02:00
len
50ae08ed8d Back button now returns to library. Closes #252 2016-04-21 16:31:23 +02:00
len
5385642a5b Downloads now retry requests after some seconds. Closes #271 2016-04-21 15:57:47 +02:00
len
0a27d4e185 Add an option to reencode images under the advanced tab. #262 2016-04-21 15:31:07 +02:00
len
bd8b9febd2 Minor changes 2016-04-21 01:04:46 +02:00
len
a30705f197 Oops... nobody noticed being unlogged from batoto? 2016-04-20 17:31:31 +02:00
len
877032a757 Fix incomplete downloads. Closes #264 2016-04-20 17:10:10 +02:00
len
19bf47b6d2 Release resources before trying to delete an incomplete file #264 #211 2016-04-19 21:04:28 +02:00
len
a9bfeb058b Revert "Temporarily include nucleus in the project"
This reverts commit 447dfd1e3c.
2016-04-19 14:11:03 +02:00
len
9213fc6999 Always close response body 2016-04-19 14:08:35 +02:00
len
447dfd1e3c Temporarily include nucleus in the project 2016-04-19 12:58:33 +02:00
len
638d3a32cf Also use manga per row setting in catalogue 2016-04-18 20:33:09 +02:00
len
17c59657c3 Allow to unbind manga, closes #258. Fix some network calls leaking 2016-04-18 20:14:50 +02:00
len
81bce8ef76 Mark common categories when moving them. Closes #135 2016-04-18 19:20:14 +02:00
len
78314077bb Fix custom brightness turning off the screen. #106 2016-04-18 17:36:27 +02:00
len
a7840bc247 Rewrite PreferencesHelper. Allow to customize navigation with volume keys and tapping. Closes #251 and closes #129. 2016-04-18 17:29:46 +02:00
6d0254c5e5 Fixed backup/restore for 3rd party applications (#255)
* Same MIME for restore/backup, so 3rd party applications like Google Drive find the proper files.
MIME changed to proper type for json files.

* MIME type for restore temporaly on "application/*" so the cached file can also be chosen
2016-04-18 13:40:34 +02:00
06681a3db7 squid:S1854 - Dead stores should be removed (#253) 2016-04-18 13:40:26 +02:00
82 changed files with 1417 additions and 1043 deletions

View File

@ -1,6 +1,6 @@
| Build | Download | Auto Update | | Build | Download | Auto Update |
|-------|----------|-------------| |-------|----------|-------------|
| [![TeamCity (simple build status)](https://img.shields.io/teamcity/https/teamcity.kanade.eu/s/tachiyomi_Build.svg)](https://teamcity.kanade.eu/project.html?projectId=tachiyomi) [![Travis](https://img.shields.io/travis/inorichi/tachiyomi.svg)](https://travis-ci.org/inorichi/tachiyomi) | [![stable release](https://img.shields.io/badge/stable-v0.2.0-blue.svg)](https://github.com/inorichi/tachiyomi/releases) [![latest dev build](https://img.shields.io/badge/dev-latest%20build-blue.svg)](http://tachiyomi.kanade.eu/latest/app-debug.apk) | [![fdroid release](https://img.shields.io/badge/stable-F--Droid-blue.svg)](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi) [![fdroid debug](https://img.shields.io/badge/dev-F--Droid-blue.svg)](//github.com/inorichi/tachiyomi/wiki/FDroid-for-debug-versions) | | [![TeamCity (simple build status)](https://img.shields.io/teamcity/https/teamcity.kanade.eu/s/tachiyomi_Build.svg)](https://teamcity.kanade.eu/project.html?projectId=tachiyomi) [![Travis](https://img.shields.io/travis/inorichi/tachiyomi.svg)](https://travis-ci.org/inorichi/tachiyomi) | [![stable release](https://img.shields.io/badge/stable-v0.2.1-blue.svg)](https://github.com/inorichi/tachiyomi/releases) [![latest dev build](https://img.shields.io/badge/dev-latest%20build-blue.svg)](http://tachiyomi.kanade.eu/latest/app-debug.apk) | [![fdroid release](https://img.shields.io/badge/stable-F--Droid-blue.svg)](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi) [![fdroid debug](https://img.shields.io/badge/dev-F--Droid-blue.svg)](//github.com/inorichi/tachiyomi/wiki/FDroid-for-debug-versions) |
## [Report an issue](https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md) ## [Report an issue](https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md)

View File

@ -38,8 +38,8 @@ android {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 23 targetSdkVersion 23
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
versionCode 6 versionCode 7
versionName "0.2.0" versionName "0.2.1"
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\"" buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\"" buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
@ -142,7 +142,7 @@ dependencies {
kapt "com.pushtorefresh.storio:sqlite-annotations-processor:$STORIO_VERSION" kapt "com.pushtorefresh.storio:sqlite-annotations-processor:$STORIO_VERSION"
// Model View Presenter // Model View Presenter
compile 'info.android15.nucleus:nucleus:2.0.5' compile 'info.android15.nucleus:nucleus:3.0.0-beta'
// Dependency injection // Dependency injection
compile "com.google.dagger:dagger:$DAGGER_VERSION" compile "com.google.dagger:dagger:$DAGGER_VERSION"

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"> package="eu.kanade.tachiyomi">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@ -68,7 +68,19 @@
</receiver> </receiver>
<receiver <receiver
android:name=".data.library.LibraryUpdateService$LibraryUpdateReceiver"> android:name=".data.library.LibraryUpdateService$SyncOnPowerConnected"
android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.ACTION_POWER_CONNECTED" />
</intent-filter>
</receiver>
<receiver
android:name=".data.library.LibraryUpdateService$CancelUpdateReceiver">
</receiver>
<receiver
android:name=".data.updater.UpdateDownloader$InstallOnReceived">
</receiver> </receiver>
<receiver <receiver
@ -79,6 +91,15 @@
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver
android:name=".data.updater.UpdateDownloaderAlarm">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="eu.kanade.CHECK_UPDATE"/>
</intent-filter>
</receiver>
<meta-data <meta-data
android:name="eu.kanade.tachiyomi.data.cache.CoverGlideModule" android:name="eu.kanade.tachiyomi.data.cache.CoverGlideModule"
android:value="GlideModule" /> android:value="GlideModule" />

View File

@ -7,6 +7,7 @@ import com.google.gson.reflect.TypeToken
import com.jakewharton.disklrucache.DiskLruCache import com.jakewharton.disklrucache.DiskLruCache
import eu.kanade.tachiyomi.data.source.model.Page import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.util.DiskUtils import eu.kanade.tachiyomi.util.DiskUtils
import eu.kanade.tachiyomi.util.saveImageTo
import okhttp3.Response import okhttp3.Response
import okio.Okio import okio.Okio
import rx.Observable import rx.Observable
@ -184,7 +185,7 @@ class ChapterCache(private val context: Context) {
* @throws IOException image error. * @throws IOException image error.
*/ */
@Throws(IOException::class) @Throws(IOException::class)
fun putImageToCache(imageUrl: String, response: Response) { fun putImageToCache(imageUrl: String, response: Response, reencode: Boolean) {
// Initialize editor (edits the values for an entry). // Initialize editor (edits the values for an entry).
var editor: DiskLruCache.Editor? = null var editor: DiskLruCache.Editor? = null
@ -194,17 +195,12 @@ class ChapterCache(private val context: Context) {
editor = diskCache.edit(key) ?: throw IOException("Unable to edit key") editor = diskCache.edit(key) ?: throw IOException("Unable to edit key")
// Get OutputStream and write image with Okio. // Get OutputStream and write image with Okio.
Okio.buffer(Okio.sink(editor.newOutputStream(0))).use { response.body().source().saveImageTo(editor.newOutputStream(0), reencode)
it.writeAll(response.body().source())
it.flush()
}
diskCache.flush() diskCache.flush()
editor.commit() editor.commit()
} catch (e: Exception) {
response.body().close()
throw IOException("Unable to save image")
} finally { } finally {
response.body().close()
editor?.abortUnlessCommitted() editor?.abortUnlessCommitted()
} }
} }

View File

@ -33,7 +33,7 @@ class CoverCache(private val context: Context) {
* @param headers headers included in Glide request. * @param headers headers included in Glide request.
* @param onReady function to call when the image is ready * @param onReady function to call when the image is ready
*/ */
fun save(thumbnailUrl: String?, headers: LazyHeaders, onReady: ((File) -> Unit)? = null) { fun save(thumbnailUrl: String?, headers: LazyHeaders?, onReady: ((File) -> Unit)? = null) {
// Check if url is empty. // Check if url is empty.
if (thumbnailUrl.isNullOrEmpty()) if (thumbnailUrl.isNullOrEmpty())
return return
@ -62,7 +62,7 @@ class CoverCache(private val context: Context) {
* @param headers headers included in Glide request. * @param headers headers included in Glide request.
* @param onReady function to call when the image is ready * @param onReady function to call when the image is ready
*/ */
fun saveOrLoadFromCache(thumbnailUrl: String?, headers: LazyHeaders, onReady: ((File) -> Unit)?) { fun saveOrLoadFromCache(thumbnailUrl: String?, headers: LazyHeaders?, onReady: ((File) -> Unit)?) {
// Check if url is empty. // Check if url is empty.
if (thumbnailUrl.isNullOrEmpty()) if (thumbnailUrl.isNullOrEmpty())
return return

View File

@ -260,6 +260,14 @@ open class DatabaseHelper(context: Context) {
fun deleteMangaSync(manga: MangaSync) = db.delete().`object`(manga).prepare() fun deleteMangaSync(manga: MangaSync) = db.delete().`object`(manga).prepare()
fun deleteMangaSyncForManga(manga: Manga) = db.delete()
.byQuery(DeleteQuery.builder()
.table(MangaSyncTable.TABLE)
.where("${MangaSyncTable.COLUMN_MANGA_ID} = ?")
.whereArgs(manga.id)
.build())
.prepare()
// Categories related queries // Categories related queries
fun getCategories() = db.get() fun getCategories() = db.get()

View File

@ -95,6 +95,13 @@ public class Manga implements Serializable {
return m; return m;
} }
public static Manga create(String pathUrl, int source) {
Manga m = new Manga();
m.url = pathUrl;
m.source = source;
return m;
}
public void setUrl(String url) { public void setUrl(String url) {
this.url = UrlUtil.getPath(url); this.url = UrlUtil.getPath(url);
} }

View File

@ -10,14 +10,12 @@ 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.data.download.model.DownloadQueue import eu.kanade.tachiyomi.data.download.model.DownloadQueue
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.source.SourceManager import eu.kanade.tachiyomi.data.source.SourceManager
import eu.kanade.tachiyomi.data.source.base.Source import eu.kanade.tachiyomi.data.source.base.Source
import eu.kanade.tachiyomi.data.source.model.Page import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.event.DownloadChaptersEvent import eu.kanade.tachiyomi.event.DownloadChaptersEvent
import eu.kanade.tachiyomi.util.DiskUtils import eu.kanade.tachiyomi.util.*
import eu.kanade.tachiyomi.util.DynamicConcurrentMergeOperator
import eu.kanade.tachiyomi.util.UrlUtil
import eu.kanade.tachiyomi.util.toast
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
@ -26,10 +24,9 @@ import rx.subjects.BehaviorSubject
import rx.subjects.PublishSubject import rx.subjects.PublishSubject
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.io.FileOutputStream
import java.io.FileReader import java.io.FileReader
import java.io.IOException
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit
class DownloadManager(private val context: Context, private val sourceManager: SourceManager, private val preferences: PreferencesHelper) { class DownloadManager(private val context: Context, private val sourceManager: SourceManager, private val preferences: PreferencesHelper) {
@ -175,7 +172,7 @@ class DownloadManager(private val context: Context, private val sourceManager: S
// Or if the page list already exists, start from the file // Or if the page list already exists, start from the file
Observable.just(download.pages) Observable.just(download.pages)
return Observable.defer<Download> { pageListObservable return Observable.defer { pageListObservable
.doOnNext { pages -> .doOnNext { pages ->
download.downloadedImages = 0 download.downloadedImages = 0
download.status = Download.DOWNLOADING download.status = Download.DOWNLOADING
@ -231,10 +228,21 @@ class DownloadManager(private val context: Context, private val sourceManager: S
private fun downloadImage(page: Page, source: Source, directory: File, filename: String): Observable<Page> { private fun downloadImage(page: Page, source: Source, directory: File, filename: String): Observable<Page> {
page.status = Page.DOWNLOAD_IMAGE page.status = Page.DOWNLOAD_IMAGE
return source.getImageProgressResponse(page) return source.getImageProgressResponse(page)
.flatMap({ resp -> .flatMap {
DiskUtils.saveBufferedSourceToDirectory(resp.body().source(), directory, filename) try {
val file = File(directory, filename)
file.parentFile.mkdirs()
it.body().source().saveImageTo(file.outputStream(), preferences.reencodeImage())
} catch (e: Exception) {
it.body().close()
throw e
}
Observable.just(page) Observable.just(page)
}).retry(2) }
.retryWhen {
it.zipWith(Observable.range(1, 3)) { errors, retries -> retries }
.flatMap { retries -> Observable.timer((retries * 2).toLong(), TimeUnit.SECONDS) }
}
} }
// Public method to get the image from the filesystem. It does NOT provide any way to download the image // Public method to get the image from the filesystem. It does NOT provide any way to download the image
@ -306,26 +314,14 @@ class DownloadManager(private val context: Context, private val sourceManager: S
val chapterDir = getAbsoluteChapterDirectory(source, manga, chapter) val chapterDir = getAbsoluteChapterDirectory(source, manga, chapter)
val pagesFile = File(chapterDir, PAGE_LIST_FILE) val pagesFile = File(chapterDir, PAGE_LIST_FILE)
var reader: JsonReader? = null return try {
try { JsonReader(FileReader(pagesFile)).use {
if (pagesFile.exists()) { val collectionType = object : TypeToken<List<Page>>() {}.type
reader = JsonReader(FileReader(pagesFile.absolutePath)) gson.fromJson(it, collectionType)
val collectionType = object : TypeToken<List<Page>>() {
}.type
return gson.fromJson<List<Page>>(reader, collectionType)
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e.cause, e.message) null
} finally {
if (reader != null) try {
reader.close()
} catch (e: IOException) {
/* Do nothing */
} }
}
return null
} }
// Shortcut for the method above // Shortcut for the method above
@ -338,20 +334,13 @@ class DownloadManager(private val context: Context, private val sourceManager: S
val chapterDir = getAbsoluteChapterDirectory(source, manga, chapter) val chapterDir = getAbsoluteChapterDirectory(source, manga, chapter)
val pagesFile = File(chapterDir, PAGE_LIST_FILE) val pagesFile = File(chapterDir, PAGE_LIST_FILE)
var out: FileOutputStream? = null pagesFile.outputStream().use {
try { try {
out = FileOutputStream(pagesFile) it.write(gson.toJson(pages).toByteArray())
out.write(gson.toJson(pages).toByteArray()) it.flush()
out.flush() } catch (e: Exception) {
} catch (e: IOException) { Timber.e(e, e.message)
Timber.e(e.cause, e.message)
} finally {
if (out != null) try {
out.close()
} catch (e: IOException) {
/* Do nothing */
} }
} }
} }
@ -365,7 +354,7 @@ class DownloadManager(private val context: Context, private val sourceManager: S
File.separator + File.separator +
manga.title.replace("[^\\sa-zA-Z0-9.-]".toRegex(), "_") manga.title.replace("[^\\sa-zA-Z0-9.-]".toRegex(), "_")
return File(preferences.downloadsDirectory, mangaRelativePath) return File(preferences.downloadsDirectory().getOrDefault(), mangaRelativePath)
} }
// Get the absolute path to the chapter directory // Get the absolute path to the chapter directory

View File

@ -4,9 +4,9 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.source.model.Page import eu.kanade.tachiyomi.data.source.model.Page
import rx.Observable import rx.Observable
import rx.subjects.PublishSubject import rx.subjects.PublishSubject
import java.util.* import java.util.concurrent.CopyOnWriteArrayList
class DownloadQueue : ArrayList<Download>() { class DownloadQueue : CopyOnWriteArrayList<Download>() {
private val statusSubject = PublishSubject.create<Download>() private val statusSubject = PublishSubject.create<Download>()

View File

@ -9,6 +9,8 @@ import android.os.IBinder
import android.os.PowerManager import android.os.PowerManager
import android.support.v4.app.NotificationCompat import android.support.v4.app.NotificationCompat
import android.util.Pair import android.util.Pair
import com.github.pwittchen.reactivenetwork.library.ConnectivityStatus
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork
import eu.kanade.tachiyomi.App import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
@ -16,42 +18,14 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.source.SourceManager import eu.kanade.tachiyomi.data.source.SourceManager
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.AndroidComponentUtil import eu.kanade.tachiyomi.util.*
import eu.kanade.tachiyomi.util.DeviceUtil
import eu.kanade.tachiyomi.util.notification
import eu.kanade.tachiyomi.util.notificationManager
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import timber.log.Timber
import java.util.* import java.util.*
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import javax.inject.Inject import javax.inject.Inject
// Intent key for forced library update
val UPDATE_IS_FORCED = "is_forced"
/**
* Get the start intent for [LibraryUpdateService].
* @param context the application context.
* @param isForced true when forcing library update
* @return the intent of the service.
*/
fun getIntent(context: Context, isForced: Boolean = false): Intent {
return Intent(context, LibraryUpdateService::class.java).apply {
putExtra(UPDATE_IS_FORCED, isForced)
}
}
/**
* 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, LibraryUpdateService::class.java)
}
/** /**
* This class will take care of updating the chapters of the manga from the library. It can be * This class will take care of updating the chapters of the manga from the library. It can be
* started calling the [start] method. If it's already running, it won't do anything. * started calling the [start] method. If it's already running, it won't do anything.
@ -77,6 +51,30 @@ class LibraryUpdateService : Service() {
companion object { companion object {
val UPDATE_NOTIFICATION_ID = 1 val UPDATE_NOTIFICATION_ID = 1
// Intent key for manual library update
val UPDATE_IS_MANUAL = "is_manual"
/**
* Get the start intent for [LibraryUpdateService].
* @param context the application context.
* @param isManual true when user triggers library update.
* @return the intent of the service.
*/
fun getIntent(context: Context, isManual: Boolean = false): Intent {
return Intent(context, LibraryUpdateService::class.java).apply {
putExtra(UPDATE_IS_MANUAL, isManual)
}
}
/**
* 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, LibraryUpdateService::class.java)
}
/** /**
* Static method to start the service. It will be started only if there isn't another * Static method to start the service. It will be started only if there isn't another
* instance already running. * instance already running.
@ -125,36 +123,52 @@ class LibraryUpdateService : Service() {
/** /**
* Method called when the service receives an intent. In this case, the content of the intent * Method called when the service receives an intent.
* is irrelevant, because everything required is fetched in [updateLibrary]. * @param intent the start intent from.
* @param intent the intent from [start].
* @param flags the flags of the command. * @param flags the flags of the command.
* @param startId the start id of this command. * @param startId the start id of this command.
* @return the start value of the command. * @return the start value of the command.
*/ */
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// If there's no network available, set a component to start this service again when
// a connection is available. // Get connectivity status
if (!DeviceUtil.isNetworkConnected(this)) { val connection = ReactiveNetwork().getConnectivityStatus(this, true)
Timber.i("Sync canceled, connection not available")
showWarningNotification(getString(R.string.notification_no_connection_title), // Get library update restrictions
getString(R.string.notification_no_connection_body)) val restrictions = preferences.libraryUpdateRestriction()
// Check if users updates library manual
val isManualUpdate = intent?.getBooleanExtra(UPDATE_IS_MANUAL, false) ?: false
// Whether to cancel the update.
var cancelUpdate = false
// Check if device has internet connection
// Check if device has wifi connection if only wifi is enabled
if (connection == ConnectivityStatus.OFFLINE || ("wifi" in restrictions
&& connection != ConnectivityStatus.WIFI_CONNECTED_HAS_INTERNET)) {
if (isManualUpdate) {
toast(R.string.notification_no_connection_title)
}
// Enable library update when connection available
AndroidComponentUtil.toggleComponent(this, SyncOnConnectionAvailable::class.java, true) AndroidComponentUtil.toggleComponent(this, SyncOnConnectionAvailable::class.java, true)
cancelUpdate = true
}
if (!isManualUpdate && "ac" in restrictions && !DeviceUtil.isPowerConnected(this)) {
AndroidComponentUtil.toggleComponent(this, SyncOnPowerConnected::class.java, true)
cancelUpdate = true
}
if (cancelUpdate) {
stopSelf(startId) stopSelf(startId)
return Service.START_NOT_STICKY return Service.START_NOT_STICKY
} }
// If user doesn't want to update while phone is not charging, cancel sync // Stop enabled components.
else if (preferences.updateOnlyWhenCharging() && !(intent?.getBooleanExtra(UPDATE_IS_FORCED, false) ?: false) && !DeviceUtil.isPowerConnected(this)) { AndroidComponentUtil.toggleComponent(this, SyncOnConnectionAvailable::class.java, false)
Timber.i("Sync canceled, not connected to ac power") AndroidComponentUtil.toggleComponent(this, SyncOnPowerConnected::class.java, false)
// Create force library update intent
val forceIntent = getLibraryUpdateReceiverIntent(LibraryUpdateReceiver.FORCE_LIBRARY_UPDATE)
// Show warning
showWarningNotification(getString(R.string.notification_not_connected_to_ac_title),
getString(R.string.notification_not_connected_to_ac_body), forceIntent)
stopSelf(startId)
return Service.START_NOT_STICKY
}
// Unsubscribe from any previous subscription if needed. // Unsubscribe from any previous subscription if needed.
subscription?.unsubscribe() subscription?.unsubscribe()
@ -173,15 +187,6 @@ class LibraryUpdateService : Service() {
return Service.START_STICKY return Service.START_STICKY
} }
/**
* Creates a PendingIntent for LibraryUpdate broadcast class
* @param action id of action
*/
fun getLibraryUpdateReceiverIntent(action: String): PendingIntent {
return PendingIntent.getBroadcast(this, 0,
Intent(this, LibraryUpdateReceiver::class.java).apply { this.action = action }, 0)
}
/** /**
* Method that updates the library. It's called in a background thread, so it's safe to do * Method that updates the library. It's called in a background thread, so it's safe to do
* heavy operations or network calls here. * heavy operations or network calls here.
@ -195,7 +200,8 @@ class LibraryUpdateService : Service() {
val newUpdates = ArrayList<Manga>() val newUpdates = ArrayList<Manga>()
val failedUpdates = ArrayList<Manga>() val failedUpdates = ArrayList<Manga>()
val cancelIntent = getLibraryUpdateReceiverIntent(LibraryUpdateReceiver.CANCEL_LIBRARY_UPDATE) val cancelIntent = PendingIntent.getBroadcast(this, 0,
Intent(this, CancelUpdateReceiver::class.java), 0)
// Get the manga list that is going to be updated. // Get the manga list that is going to be updated.
val allLibraryMangas = db.getFavoriteMangas().executeAsBlocking() val allLibraryMangas = db.getFavoriteMangas().executeAsBlocking()
@ -299,12 +305,11 @@ class LibraryUpdateService : Service() {
* @param body the body of the notification. * @param body the body of the notification.
*/ */
private fun showNotification(title: String, body: String) { private fun showNotification(title: String, body: String) {
val n = notification() { notificationManager.notify(UPDATE_NOTIFICATION_ID, notification() {
setSmallIcon(R.drawable.ic_refresh_white_24dp_img) setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
setContentTitle(title) setContentTitle(title)
setContentText(body) setContentText(body)
} })
notificationManager.notify(UPDATE_NOTIFICATION_ID, n)
} }
/** /**
@ -314,35 +319,15 @@ class LibraryUpdateService : Service() {
* @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, cancelIntent: PendingIntent) {
val n = notification() { notificationManager.notify(UPDATE_NOTIFICATION_ID, notification() {
setSmallIcon(R.drawable.ic_refresh_white_24dp_img) setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
setContentTitle(manga.title) setContentTitle(manga.title)
setProgress(total, current, false) setProgress(total, current, false)
setOngoing(true) setOngoing(true)
addAction(R.drawable.ic_clear_grey_24dp_img, getString(android.R.string.cancel), cancelIntent) addAction(R.drawable.ic_clear_grey_24dp_img, getString(android.R.string.cancel), cancelIntent)
} })
notificationManager.notify(UPDATE_NOTIFICATION_ID, n)
} }
/**
* Show warning message when library can't be updated
* @param warningTitle title of warning
* @param warningBody warning information
* @param pendingIntent Intent called when action clicked
*/
private fun showWarningNotification(warningTitle: String, warningBody: String, pendingIntent: PendingIntent? = null) {
val n = notification() {
setSmallIcon(R.drawable.ic_warning_white_24dp_img)
setContentTitle(warningTitle)
setStyle(NotificationCompat.BigTextStyle().bigText(warningBody))
setContentIntent(notificationIntent)
if (pendingIntent != null) {
addAction(R.drawable.ic_refresh_grey_24dp_img, getString(R.string.action_force), pendingIntent)
}
setAutoCancel(true)
}
notificationManager.notify(UPDATE_NOTIFICATION_ID, n)
}
/** /**
* 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.
@ -353,14 +338,13 @@ class LibraryUpdateService : Service() {
val title = getString(R.string.notification_update_completed) val title = getString(R.string.notification_update_completed)
val body = getUpdatedMangasBody(updates, failed) val body = getUpdatedMangasBody(updates, failed)
val n = notification() { notificationManager.notify(UPDATE_NOTIFICATION_ID, notification() {
setSmallIcon(R.drawable.ic_refresh_white_24dp_img) setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
setContentTitle(title) setContentTitle(title)
setStyle(NotificationCompat.BigTextStyle().bigText(body)) setStyle(NotificationCompat.BigTextStyle().bigText(body))
setContentIntent(notificationIntent) setContentIntent(notificationIntent)
setAutoCancel(true) setAutoCancel(true)
} })
notificationManager.notify(UPDATE_NOTIFICATION_ID, n)
} }
/** /**
@ -385,7 +369,6 @@ class LibraryUpdateService : Service() {
* network changes. * network changes.
*/ */
class SyncOnConnectionAvailable : BroadcastReceiver() { class SyncOnConnectionAvailable : BroadcastReceiver() {
/** /**
* Method called when a network change occurs. * Method called when a network change occurs.
* @param context the application context. * @param context the application context.
@ -394,36 +377,39 @@ class LibraryUpdateService : Service() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
if (DeviceUtil.isNetworkConnected(context)) { if (DeviceUtil.isNetworkConnected(context)) {
AndroidComponentUtil.toggleComponent(context, this.javaClass, false) AndroidComponentUtil.toggleComponent(context, this.javaClass, false)
context.startService(getIntent(context)) start(context)
} }
} }
} }
/** /**
* Class that triggers the library to update. * Class that triggers the library to update when connected to power.
*/ */
class LibraryUpdateReceiver : BroadcastReceiver() { class SyncOnPowerConnected: BroadcastReceiver() {
companion object { /**
// Cancel library update action * Method called when AC is connected.
val CANCEL_LIBRARY_UPDATE = "eu.kanade.CANCEL_LIBRARY_UPDATE" * @param context the application context.
// Force library update * @param intent the intent received.
val FORCE_LIBRARY_UPDATE = "eu.kanade.FORCE_LIBRARY_UPDATE" */
override fun onReceive(context: Context, intent: Intent) {
AndroidComponentUtil.toggleComponent(context, this.javaClass, false)
start(context)
}
} }
/**
* Class that stops updating the library.
*/
class CancelUpdateReceiver : BroadcastReceiver() {
/** /**
* Method called when user wants a library update. * Method called when user wants a library update.
* @param context the application context. * @param context the application context.
* @param intent the intent received. * @param intent the intent received.
*/ */
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
CANCEL_LIBRARY_UPDATE -> {
LibraryUpdateService.stop(context) LibraryUpdateService.stop(context)
context.notificationManager.cancel(UPDATE_NOTIFICATION_ID) context.notificationManager.cancel(UPDATE_NOTIFICATION_ID)
} }
FORCE_LIBRARY_UPDATE -> LibraryUpdateService.start(context, true)
}
}
} }
} }

View File

@ -24,8 +24,8 @@ abstract class MangaSyncService(private val context: Context, val id: Int) {
abstract fun login(username: String, password: String): Observable<Boolean> abstract fun login(username: String, password: String): Observable<Boolean>
open val isLogged: Boolean open val isLogged: Boolean
get() = !preferences.getMangaSyncUsername(this).isEmpty() && get() = !preferences.mangaSyncUsername(this).isEmpty() &&
!preferences.getMangaSyncPassword(this).isEmpty() !preferences.mangaSyncPassword(this).isEmpty()
abstract fun update(manga: MangaSync): Observable<Response> abstract fun update(manga: MangaSync): Observable<Response>

View File

@ -46,8 +46,8 @@ class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(cont
} }
init { init {
val username = preferences.getMangaSyncUsername(this) val username = preferences.mangaSyncUsername(this)
val password = preferences.getMangaSyncPassword(this) val password = preferences.mangaSyncPassword(this)
if (!username.isEmpty() && !password.isEmpty()) { if (!username.isEmpty() && !password.isEmpty()) {
createHeaders(username, password) createHeaders(username, password)

View File

@ -10,10 +10,13 @@ import java.net.CookieStore
class NetworkHelper(context: Context) { class NetworkHelper(context: Context) {
private val client: OkHttpClient private val cacheDir = File(context.cacheDir, "network_cache")
private val forceCacheClient: OkHttpClient
private val cookieManager: CookieManager private val cacheSize = 5L * 1024 * 1024 // 5 MiB
private val cookieManager = CookieManager().apply {
setCookiePolicy(CookiePolicy.ACCEPT_ALL)
}
private val forceCacheInterceptor = { chain: Interceptor.Chain -> private val forceCacheInterceptor = { chain: Interceptor.Chain ->
val originalResponse = chain.proceed(chain.request()) val originalResponse = chain.proceed(chain.request())
@ -23,41 +26,39 @@ class NetworkHelper(context: Context) {
.build() .build()
} }
private val cacheSize = 5L * 1024 * 1024 // 5 MiB private val client = OkHttpClient.Builder()
private val cacheDir = "network_cache"
init {
val cacheDir = File(context.cacheDir, cacheDir)
cookieManager = CookieManager()
cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL)
client = OkHttpClient.Builder()
.cookieJar(JavaNetCookieJar(cookieManager)) .cookieJar(JavaNetCookieJar(cookieManager))
.cache(Cache(cacheDir, cacheSize)) .cache(Cache(cacheDir, cacheSize))
.build() .build()
forceCacheClient = client.newBuilder() private val forceCacheClient = client.newBuilder()
.addNetworkInterceptor(forceCacheInterceptor) .addNetworkInterceptor(forceCacheInterceptor)
.build() .build()
}
val cookies: CookieStore
get() = cookieManager.cookieStore
@JvmOverloads @JvmOverloads
fun request(request: Request, forceCache: Boolean = false): Observable<Response> { fun request(request: Request, forceCache: Boolean = false): Observable<Response> {
return Observable.fromCallable { return Observable.fromCallable {
val c = if (forceCache) forceCacheClient else client val c = if (forceCache) forceCacheClient else client
c.newCall(request).execute() c.newCall(request).execute().apply { body().close() }
} }
} }
@JvmOverloads @JvmOverloads
fun requestBody(request: Request, forceCache: Boolean = false): Observable<String> { fun requestBody(request: Request, forceCache: Boolean = false): Observable<String> {
return request(request, forceCache) return Observable.fromCallable {
.map { it.body().string() } val c = if (forceCache) forceCacheClient else client
c.newCall(request).execute().body().string()
}
} }
fun requestBodyProgress(request: Request, listener: ProgressListener): Observable<Response> { fun requestBodyProgress(request: Request, listener: ProgressListener): Observable<Response> {
return Observable.fromCallable { return Observable.fromCallable { requestBodyProgressBlocking(request, listener) }
}
fun requestBodyProgressBlocking(request: Request, listener: ProgressListener): Response {
val progressClient = client.newBuilder() val progressClient = client.newBuilder()
.cache(null) .cache(null)
.addNetworkInterceptor { chain -> .addNetworkInterceptor { chain ->
@ -68,11 +69,8 @@ class NetworkHelper(context: Context) {
} }
.build() .build()
progressClient.newCall(request).execute() return progressClient.newCall(request).execute()
}.retry(1)
} }
val cookies: CookieStore
get() = cookieManager.cookieStore
} }

View File

@ -0,0 +1,90 @@
package eu.kanade.tachiyomi.data.preference
import android.content.Context
import eu.kanade.tachiyomi.R
/**
* This class stores the keys for the preferences in the application. Most of them are defined
* in the file "keys.xml". By using this class we can define preferences in one place and get them
* referenced here.
*/
class PreferenceKeys(context: Context) {
val rotation = context.getString(R.string.pref_rotation_type_key)
val enableTransitions = context.getString(R.string.pref_enable_transitions_key)
val showPageNumber = context.getString(R.string.pref_show_page_number_key)
val hideStatusBar = context.getString(R.string.pref_hide_status_bar_key)
val keepScreenOn = context.getString(R.string.pref_keep_screen_on_key)
val customBrightness = context.getString(R.string.pref_custom_brightness_key)
val customBrightnessValue = context.getString(R.string.pref_custom_brightness_value_key)
val defaultViewer = context.getString(R.string.pref_default_viewer_key)
val imageScaleType = context.getString(R.string.pref_image_scale_type_key)
val imageDecoder = context.getString(R.string.pref_image_decoder_key)
val zoomStart = context.getString(R.string.pref_zoom_start_key)
val readerTheme = context.getString(R.string.pref_reader_theme_key)
val readWithTapping = context.getString(R.string.pref_read_with_tapping_key)
val readWithVolumeKeys = context.getString(R.string.pref_read_with_volume_keys_key)
val reencodeImage = context.getString(R.string.pref_reencode_key)
val portraitColumns = context.getString(R.string.pref_library_columns_portrait_key)
val landscapeColumns = context.getString(R.string.pref_library_columns_landscape_key)
val updateOnlyNonCompleted = context.getString(R.string.pref_update_only_non_completed_key)
val autoUpdateMangaSync = context.getString(R.string.pref_auto_update_manga_sync_key)
val askUpdateMangaSync = context.getString(R.string.pref_ask_update_manga_sync_key)
val lastUsedCatalogueSource = context.getString(R.string.pref_last_catalogue_source_key)
val seamlessMode = context.getString(R.string.pref_seamless_mode_key)
val catalogueAsList = context.getString(R.string.pref_display_catalogue_as_list)
val enabledLanguages = context.getString(R.string.pref_source_languages)
val downloadsDirectory = context.getString(R.string.pref_download_directory_key)
val downloadThreads = context.getString(R.string.pref_download_slots_key)
val downloadOnlyOverWifi = context.getString(R.string.pref_download_only_over_wifi_key)
val removeAfterRead = context.getString(R.string.pref_remove_after_read_key)
val removeAfterReadPrevious = context.getString(R.string.pref_remove_after_read_previous_key)
val removeAfterMarkedAsRead = context.getString(R.string.pref_remove_after_marked_as_read_key)
val libraryUpdateInterval = context.getString(R.string.pref_library_update_interval_key)
val libraryUpdateRestriction = context.getString(R.string.pref_library_update_restriction_key)
val filterDownloaded = context.getString(R.string.pref_filter_downloaded_key)
val filterUnread = context.getString(R.string.pref_filter_unread_key)
fun sourceUsername(sourceId: Int) = "pref_source_username_$sourceId"
fun sourcePassword(sourceId: Int) = "pref_source_password_$sourceId"
fun syncUsername(syncId: Int) = "pref_mangasync_username_$syncId"
fun syncPassword(syncId: Int) = "pref_mangasync_password_$syncId"
}

View File

@ -15,36 +15,35 @@ fun <T> Preference<T>.getOrDefault(): T = get() ?: defaultValue()!!
class PreferencesHelper(private val context: Context) { class PreferencesHelper(private val context: Context) {
val keys = PreferenceKeys(context)
private val prefs = PreferenceManager.getDefaultSharedPreferences(context) private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
private val rxPrefs = RxSharedPreferences.create(prefs) private val rxPrefs = RxSharedPreferences.create(prefs)
private val defaultDownloadsDir: File private val defaultDownloadsDir = File(Environment.getExternalStorageDirectory().absolutePath +
init {
defaultDownloadsDir = File(Environment.getExternalStorageDirectory().absolutePath +
File.separator + context.getString(R.string.app_name), "downloads") File.separator + context.getString(R.string.app_name), "downloads")
init {
// Don't display downloaded chapters in gallery apps creating a ".nomedia" file // Don't display downloaded chapters in gallery apps creating a ".nomedia" file
try { try {
File(downloadsDirectory, ".nomedia").createNewFile() File(downloadsDirectory().getOrDefault(), ".nomedia").createNewFile()
} catch (e: IOException) { } catch (e: IOException) {
/* Ignore */ /* Ignore */
} }
} }
companion object { companion object {
const val SOURCE_ACCOUNT_USERNAME = "pref_source_username_"
const val SOURCE_ACCOUNT_PASSWORD = "pref_source_password_"
const val MANGASYNC_ACCOUNT_USERNAME = "pref_mangasync_username_"
const val MANGASYNC_ACCOUNT_PASSWORD = "pref_mangasync_password_"
fun getLibraryUpdateInterval(context: Context): Int { fun getLibraryUpdateInterval(context: Context): Int {
return PreferenceManager.getDefaultSharedPreferences(context).getInt( return PreferenceManager.getDefaultSharedPreferences(context).getInt(
context.getString(R.string.pref_library_update_interval_key), 0) context.getString(R.string.pref_library_update_interval_key), 0)
} }
fun getAutomaticUpdateStatus(context: Context): Boolean {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(
context.getString(R.string.pref_enable_automatic_updates), false)
}
@JvmStatic @JvmStatic
fun getTheme(context: Context): Int { fun getTheme(context: Context): Int {
return PreferenceManager.getDefaultSharedPreferences(context).getInt( return PreferenceManager.getDefaultSharedPreferences(context).getInt(
@ -52,165 +51,98 @@ class PreferencesHelper(private val context: Context) {
} }
} }
private fun getKey(keyResource: Int): String {
return context.getString(keyResource)
}
fun clear() { fun clear() {
prefs.edit().clear().apply() prefs.edit().clear().apply()
} }
fun rotation(): Preference<Int> { fun rotation() = rxPrefs.getInteger(keys.rotation, 1)
return rxPrefs.getInteger(getKey(R.string.pref_rotation_type_key), 1)
}
fun enableTransitions(): Preference<Boolean> { fun enableTransitions() = rxPrefs.getBoolean(keys.enableTransitions, true)
return rxPrefs.getBoolean(getKey(R.string.pref_enable_transitions_key), true)
}
fun showPageNumber(): Preference<Boolean> { fun showPageNumber() = rxPrefs.getBoolean(keys.showPageNumber, true)
return rxPrefs.getBoolean(getKey(R.string.pref_show_page_number_key), true)
}
fun hideStatusBar(): Preference<Boolean> { fun hideStatusBar() = rxPrefs.getBoolean(keys.hideStatusBar, true)
return rxPrefs.getBoolean(getKey(R.string.pref_hide_status_bar_key), true)
}
fun keepScreenOn(): Preference<Boolean> { fun keepScreenOn() = rxPrefs.getBoolean(keys.keepScreenOn, true)
return rxPrefs.getBoolean(getKey(R.string.pref_keep_screen_on_key), true)
}
fun customBrightness(): Preference<Boolean> { fun customBrightness() = rxPrefs.getBoolean(keys.customBrightness, false)
return rxPrefs.getBoolean(getKey(R.string.pref_custom_brightness_key), false)
}
fun customBrightnessValue(): Preference<Float> { fun customBrightnessValue() = rxPrefs.getFloat(keys.customBrightnessValue, 0f)
return rxPrefs.getFloat(getKey(R.string.pref_custom_brightness_value_key), 0f)
}
val defaultViewer: Int fun defaultViewer() = prefs.getInt(keys.defaultViewer, 1)
get() = prefs.getInt(getKey(R.string.pref_default_viewer_key), 1)
fun imageScaleType(): Preference<Int> { fun imageScaleType() = rxPrefs.getInteger(keys.imageScaleType, 1)
return rxPrefs.getInteger(getKey(R.string.pref_image_scale_type_key), 1)
}
fun imageDecoder(): Preference<Int> { fun imageDecoder() = rxPrefs.getInteger(keys.imageDecoder, 0)
return rxPrefs.getInteger(getKey(R.string.pref_image_decoder_key), 0)
}
fun zoomStart(): Preference<Int> { fun zoomStart() = rxPrefs.getInteger(keys.zoomStart, 1)
return rxPrefs.getInteger(getKey(R.string.pref_zoom_start_key), 1)
}
fun readerTheme(): Preference<Int> { fun readerTheme() = rxPrefs.getInteger(keys.readerTheme, 0)
return rxPrefs.getInteger(getKey(R.string.pref_reader_theme_key), 0)
}
fun portraitColumns(): Preference<Int> { fun readWithTapping() = rxPrefs.getBoolean(keys.readWithTapping, true)
return rxPrefs.getInteger(getKey(R.string.pref_library_columns_portrait_key), 0)
}
fun landscapeColumns(): Preference<Int> { fun readWithVolumeKeys() = rxPrefs.getBoolean(keys.readWithVolumeKeys, false)
return rxPrefs.getInteger(getKey(R.string.pref_library_columns_landscape_key), 0)
}
fun updateOnlyNonCompleted(): Boolean { fun reencodeImage() = prefs.getBoolean(keys.reencodeImage, false)
return prefs.getBoolean(getKey(R.string.pref_update_only_non_completed_key), false)
}
fun autoUpdateMangaSync(): Boolean { fun portraitColumns() = rxPrefs.getInteger(keys.portraitColumns, 0)
return prefs.getBoolean(getKey(R.string.pref_auto_update_manga_sync_key), true)
}
fun askUpdateMangaSync(): Boolean { fun landscapeColumns() = rxPrefs.getInteger(keys.landscapeColumns, 0)
return prefs.getBoolean(getKey(R.string.pref_ask_update_manga_sync_key), false)
}
fun lastUsedCatalogueSource(): Preference<Int> { fun updateOnlyNonCompleted() = prefs.getBoolean(keys.updateOnlyNonCompleted, false)
return rxPrefs.getInteger(getKey(R.string.pref_last_catalogue_source_key), -1)
}
fun seamlessMode(): Boolean { fun autoUpdateMangaSync() = prefs.getBoolean(keys.autoUpdateMangaSync, true)
return prefs.getBoolean(getKey(R.string.pref_seamless_mode_key), true)
}
fun catalogueAsList(): Preference<Boolean> { fun askUpdateMangaSync() = prefs.getBoolean(keys.askUpdateMangaSync, false)
return rxPrefs.getBoolean(getKey(R.string.pref_display_catalogue_as_list), false)
}
fun enabledLanguages(): Preference<MutableSet<String>> { fun lastUsedCatalogueSource() = rxPrefs.getInteger(keys.lastUsedCatalogueSource, -1)
return rxPrefs.getStringSet(getKey(R.string.pref_source_languages), setOf("EN"))
}
fun getSourceUsername(source: Source): String { fun seamlessMode() = prefs.getBoolean(keys.seamlessMode, true)
return prefs.getString(SOURCE_ACCOUNT_USERNAME + source.id, "")
}
fun getSourcePassword(source: Source): String { fun catalogueAsList() = rxPrefs.getBoolean(keys.catalogueAsList, false)
return prefs.getString(SOURCE_ACCOUNT_PASSWORD + source.id, "")
} fun enabledLanguages() = rxPrefs.getStringSet(keys.enabledLanguages, setOf("EN"))
fun sourceUsername(source: Source) = prefs.getString(keys.sourceUsername(source.id), "")
fun sourcePassword(source: Source) = prefs.getString(keys.sourcePassword(source.id), "")
fun setSourceCredentials(source: Source, username: String, password: String) { fun setSourceCredentials(source: Source, username: String, password: String) {
prefs.edit() prefs.edit()
.putString(SOURCE_ACCOUNT_USERNAME + source.id, username) .putString(keys.sourceUsername(source.id), username)
.putString(SOURCE_ACCOUNT_PASSWORD + source.id, password) .putString(keys.sourcePassword(source.id), password)
.apply() .apply()
} }
fun getMangaSyncUsername(sync: MangaSyncService): String { fun mangaSyncUsername(sync: MangaSyncService) = prefs.getString(keys.syncUsername(sync.id), "")
return prefs.getString(MANGASYNC_ACCOUNT_USERNAME + sync.id, "")
}
fun getMangaSyncPassword(sync: MangaSyncService): String { fun mangaSyncPassword(sync: MangaSyncService) = prefs.getString(keys.syncPassword(sync.id), "")
return prefs.getString(MANGASYNC_ACCOUNT_PASSWORD + sync.id, "")
}
fun setMangaSyncCredentials(sync: MangaSyncService, username: String, password: String) { fun setMangaSyncCredentials(sync: MangaSyncService, username: String, password: String) {
prefs.edit() prefs.edit()
.putString(MANGASYNC_ACCOUNT_USERNAME + sync.id, username) .putString(keys.syncUsername(sync.id), username)
.putString(MANGASYNC_ACCOUNT_PASSWORD + sync.id, password) .putString(keys.syncPassword(sync.id), password)
.apply() .apply()
} }
var downloadsDirectory: String fun downloadsDirectory() = rxPrefs.getString(keys.downloadsDirectory, defaultDownloadsDir.absolutePath)
get() = prefs.getString(getKey(R.string.pref_download_directory_key), defaultDownloadsDir.absolutePath)
set(path) = prefs.edit().putString(getKey(R.string.pref_download_directory_key), path).apply()
fun downloadThreads(): Preference<Int> { fun downloadThreads() = rxPrefs.getInteger(keys.downloadThreads, 1)
return rxPrefs.getInteger(getKey(R.string.pref_download_slots_key), 1)
}
fun downloadOnlyOverWifi(): Boolean { fun downloadOnlyOverWifi() = prefs.getBoolean(keys.downloadOnlyOverWifi, true)
return prefs.getBoolean(getKey(R.string.pref_download_only_over_wifi_key), true)
}
fun removeAfterRead(): Boolean { fun removeAfterRead() = prefs.getBoolean(keys.removeAfterRead, false)
return prefs.getBoolean(getKey(R.string.pref_remove_after_read_key), false)
}
fun removeAfterReadPrevious(): Boolean { fun removeAfterReadPrevious() = prefs.getBoolean(keys.removeAfterReadPrevious, false)
return prefs.getBoolean(getKey(R.string.pref_remove_after_read_previous_key), false)
}
fun removeAfterMarkedAsRead(): Boolean { fun removeAfterMarkedAsRead() = prefs.getBoolean(keys.removeAfterMarkedAsRead, false)
return prefs.getBoolean(getKey(R.string.pref_remove_after_marked_as_read_key), false)
}
fun updateOnlyWhenCharging(): Boolean { fun libraryUpdateInterval() = rxPrefs.getInteger(keys.libraryUpdateInterval, 0)
return prefs.getBoolean(getKey(R.string.pref_update_only_when_charging_key), false)
}
fun libraryUpdateInterval(): Preference<Int> { fun libraryUpdateRestriction() = prefs.getStringSet(keys.libraryUpdateRestriction, emptySet())
return rxPrefs.getInteger(getKey(R.string.pref_library_update_interval_key), 0)
}
fun filterDownloaded(): Preference<Boolean> { fun filterDownloaded() = rxPrefs.getBoolean(keys.filterDownloaded, false)
return rxPrefs.getBoolean(getKey(R.string.pref_filter_downloaded_key), false)
}
fun filterUnread(): Preference<Boolean> { fun filterUnread() = rxPrefs.getBoolean(keys.filterUnread, false)
return rxPrefs.getBoolean(getKey(R.string.pref_filter_unread_key), false)
}
} }

View File

@ -1,8 +1,8 @@
package eu.kanade.tachiyomi.data.source package eu.kanade.tachiyomi.data.source
class Language(val lang: String, val code: String) class Language(val code: String, val lang: String)
val EN = Language("English", "EN") val EN = Language("EN", "English")
val RU = Language("Russian", "RU") val RU = Language("RU", "Russian")
fun getLanguages(): List<Language> = listOf(EN, RU) fun getLanguages() = listOf(EN, RU)

View File

@ -181,13 +181,19 @@ abstract class Source(context: Context) : BaseSource() {
page.status = Page.DOWNLOAD_IMAGE page.status = Page.DOWNLOAD_IMAGE
return getImageProgressResponse(page) return getImageProgressResponse(page)
.flatMap { resp -> .flatMap { resp ->
chapterCache.putImageToCache(page.imageUrl, resp) chapterCache.putImageToCache(page.imageUrl, resp, prefs.reencodeImage())
Observable.just(page) Observable.just(page)
} }
} }
open fun getImageProgressResponse(page: Page): Observable<Response> { open fun getImageProgressResponse(page: Page): Observable<Response> {
return networkService.requestBodyProgress(imageRequest(page), page) return networkService.requestBodyProgress(imageRequest(page), page)
.doOnNext {
if (!it.isSuccessful) {
it.body().close()
throw RuntimeException("Not a valid response")
}
}
} }
fun savePageList(chapterUrl: String, pages: List<Page>?) { fun savePageList(chapterUrl: String, pages: List<Page>?) {

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.data.source.model; package eu.kanade.tachiyomi.data.source.model;
import java.io.Serializable;
import java.util.List; import java.util.List;
import eu.kanade.tachiyomi.data.database.models.Chapter; import eu.kanade.tachiyomi.data.database.models.Chapter;
@ -25,6 +24,14 @@ public class Page implements ProgressListener {
public static final int READY = 3; public static final int READY = 3;
public static final int ERROR = 4; public static final int ERROR = 4;
public Page(int pageNumber, String url) {
this(pageNumber, url, null, null);
}
public Page(int pageNumber, String url, String imageUrl) {
this(pageNumber, url, imageUrl, null);
}
public Page(int pageNumber, String url, String imageUrl, String imagePath) { public Page(int pageNumber, String url, String imageUrl, String imagePath) {
this.pageNumber = pageNumber; this.pageNumber = pageNumber;
this.url = url; this.url = url;
@ -32,10 +39,6 @@ public class Page implements ProgressListener {
this.imagePath = imagePath; this.imagePath = imagePath;
} }
public Page(int pageNumber, String url) {
this(pageNumber, url, null, null);
}
public int getPageNumber() { public int getPageNumber() {
return pageNumber; return pageNumber;
} }

View File

@ -372,8 +372,8 @@ public class Batoto extends LoginSource {
@Override @Override
public Observable<List<Chapter>> pullChaptersFromNetwork(final String mangaUrl) { public Observable<List<Chapter>> pullChaptersFromNetwork(final String mangaUrl) {
Observable<List<Chapter>> observable; Observable<List<Chapter>> observable;
String username = getPrefs().getSourceUsername(this); String username = getPrefs().sourceUsername(this);
String password = getPrefs().getSourcePassword(this); String password = getPrefs().sourcePassword(this);
if (username.isEmpty() && password.isEmpty()) { if (username.isEmpty() && password.isEmpty()) {
observable = Observable.error(new Exception("User not logged")); observable = Observable.error(new Exception("User not logged"));
} }

View File

@ -1,99 +0,0 @@
package eu.kanade.tachiyomi.data.updater;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import javax.inject.Inject;
import eu.kanade.tachiyomi.App;
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
public class UpdateDownloader extends AsyncTask<String, Void, Void> {
/**
* Name of cache directory.
*/
private static final String PARAMETER_CACHE_DIRECTORY = "apk_downloads";
/**
* Interface to global information about an application environment.
*/
private final Context context;
/**
* Cache directory used for cache management.
*/
private final File cacheDir;
@Inject PreferencesHelper preferencesHelper;
/**
* Constructor of UpdaterCache.
*
* @param context application environment interface.
*/
public UpdateDownloader(Context context) {
App.get(context).getComponent().inject(this);
this.context = context;
// Get cache directory from parameter.
cacheDir = new File(preferencesHelper.getDownloadsDirectory(), PARAMETER_CACHE_DIRECTORY);
// Create cache directory.
createCacheDir();
}
/**
* Create cache directory if it doesn't exist
*
* @return true if cache dir is created otherwise false.
*/
@SuppressWarnings("UnusedReturnValue")
private boolean createCacheDir() {
return !cacheDir.exists() && cacheDir.mkdirs();
}
@Override
protected Void doInBackground(String... args) {
try {
createCacheDir();
URL url = new URL(args[0]);
HttpURLConnection c = (HttpURLConnection) url.openConnection();
c.connect();
File outputFile = new File(cacheDir, "update.apk");
if (outputFile.exists()) {
//noinspection ResultOfMethodCallIgnored
outputFile.delete();
}
FileOutputStream fos = new FileOutputStream(outputFile);
InputStream is = c.getInputStream();
byte[] buffer = new byte[1024];
int len1;
while ((len1 = is.read(buffer)) != -1) {
fos.write(buffer, 0, len1);
}
fos.close();
is.close();
// Prompt install interface
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(outputFile), "application/vnd.android.package-archive");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // without this flag android returned a intent error!
context.startActivity(intent);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

View File

@ -0,0 +1,199 @@
package eu.kanade.tachiyomi.data.updater
import android.app.Notification
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.AsyncTask
import android.support.v4.app.NotificationCompat
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.network.NetworkHelper
import eu.kanade.tachiyomi.data.network.ProgressListener
import eu.kanade.tachiyomi.data.network.get
import eu.kanade.tachiyomi.util.notificationManager
import eu.kanade.tachiyomi.util.saveTo
import timber.log.Timber
import java.io.File
import javax.inject.Inject
class UpdateDownloader(private val context: Context) :
AsyncTask<String, Int, UpdateDownloader.DownloadResult>() {
companion object {
/**
* Prompt user with apk install intent
* @param context context
* @param file file of apk that is installed
*/
fun installAPK(context: Context, file: File) {
// Prompt install interface
val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive")
// Without this flag android returned a intent error!
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(intent)
}
}
@Inject lateinit var network: NetworkHelper
/**
* Default download dir
*/
val apkFile = File(context.externalCacheDir, "update.apk")
/**
* Notification builder
*/
val notificationBuilder = NotificationCompat.Builder(context)
init {
App.get(context).component.inject(this)
}
/**
* Class containing download result
* @param url url of file
* @param successful status of download
*/
class DownloadResult(var url: String, var successful: Boolean)
/**
* Called before downloading
*/
override fun onPreExecute() {
// Create download notification
with(notificationBuilder) {
setContentTitle(context.getString(R.string.update_check_notification_file_download))
setContentText(context.getString(R.string.update_check_notification_download_in_progress))
setSmallIcon(android.R.drawable.stat_sys_download)
}
}
override fun doInBackground(vararg params: String?): DownloadResult {
// Initialize information array containing path and url to file.
val result = DownloadResult(params[0]!!, false)
// Progress of the download
var savedProgress = 0
val progressListener = object : ProgressListener {
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
val progress = (100 * bytesRead / contentLength).toInt()
if (progress > savedProgress) {
savedProgress = progress
publishProgress(progress)
}
}
}
try {
// Make the request and download the file
val response = network.requestBodyProgressBlocking(get(result.url), progressListener)
if (response.isSuccessful) {
response.body().source().saveTo(apkFile)
// Set download successful
result.successful = true
}
} catch (e: Exception) {
Timber.e(e, e.message)
}
return result
}
/**
* Called when progress is updated
* @param values values containing progress
*/
override fun onProgressUpdate(vararg values: Int?) {
// Notify notification manager to update notification
values.getOrNull(0)?.let {
notificationBuilder.setProgress(100, it, false)
// Displays the progress bar on notification
context.notificationManager.notify(InstallOnReceived.notificationId, notificationBuilder.build())
}
}
/**
* Called when download done
* @param result string containing download information
*/
override fun onPostExecute(result: DownloadResult) {
with(notificationBuilder) {
if (result.successful) {
setContentTitle(context.getString(R.string.app_name))
setContentText(context.getString(R.string.update_check_notification_download_complete))
addAction(R.drawable.ic_system_update_grey_24dp_img, context.getString(R.string.action_install),
getInstallOnReceivedIntent(InstallOnReceived.INSTALL_APK, apkFile.absolutePath))
addAction(R.drawable.ic_clear_grey_24dp_img, context.getString(R.string.action_cancel),
getInstallOnReceivedIntent(InstallOnReceived.CANCEL_NOTIFICATION))
} else {
setContentText(context.getString(R.string.update_check_notification_download_error))
addAction(R.drawable.ic_refresh_grey_24dp_img, context.getString(R.string.action_retry),
getInstallOnReceivedIntent(InstallOnReceived.RETRY_DOWNLOAD, result.url))
addAction(R.drawable.ic_clear_grey_24dp_img, context.getString(R.string.action_cancel),
getInstallOnReceivedIntent(InstallOnReceived.CANCEL_NOTIFICATION))
}
setSmallIcon(android.R.drawable.stat_sys_download_done)
setProgress(0, 0, false)
}
val notification = notificationBuilder.build()
notification.flags = Notification.FLAG_NO_CLEAR
context.notificationManager.notify(InstallOnReceived.notificationId, notification)
}
/**
* Returns broadcast intent
* @param action action name of broadcast intent
* @param path path of file | url of file
* @return broadcast intent
*/
fun getInstallOnReceivedIntent(action: String, path: String = ""): PendingIntent {
val intent = Intent(context, InstallOnReceived::class.java).apply {
this.action = action
putExtra(InstallOnReceived.FILE_LOCATION, path)
}
return PendingIntent.getBroadcast(context, 0, intent, 0)
}
/**
* BroadcastEvent used to install apk or retry download
*/
class InstallOnReceived : BroadcastReceiver() {
companion object {
// Install apk action
val INSTALL_APK = "eu.kanade.INSTALL_APK"
// Retry download action
val RETRY_DOWNLOAD = "eu.kanade.RETRY_DOWNLOAD"
// Retry download action
val CANCEL_NOTIFICATION = "eu.kanade.CANCEL_NOTIFICATION"
// Absolute path of file || URL of file
val FILE_LOCATION = "file_location"
// Id of the notification
val notificationId = 2
}
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
// Install apk.
INSTALL_APK -> UpdateDownloader.installAPK(context, File(intent.getStringExtra(FILE_LOCATION)))
// Retry download.
RETRY_DOWNLOAD -> UpdateDownloader(context).execute(intent.getStringExtra(FILE_LOCATION))
CANCEL_NOTIFICATION -> context.notificationManager.cancel(notificationId)
}
}
}
}

View File

@ -0,0 +1,109 @@
package eu.kanade.tachiyomi.data.updater
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.SystemClock
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.DeviceUtil
import eu.kanade.tachiyomi.util.alarmManager
import eu.kanade.tachiyomi.util.notification
import eu.kanade.tachiyomi.util.notificationManager
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
class UpdateDownloaderAlarm : BroadcastReceiver() {
companion object {
const val CHECK_UPDATE_ACTION = "eu.kanade.CHECK_UPDATE"
/**
* Sets the alarm to run the intent that checks for update
* @param context the application context.
* @param intervalInHours the time in hours when it will be executed.
*/
@JvmStatic
@JvmOverloads
fun startAlarm(context: Context, intervalInHours: Int = 12, isEnabled: Boolean = PreferencesHelper.getAutomaticUpdateStatus(context)) {
// Stop previous running alarms if needed, and do not restart it if the interval is 0.
UpdateDownloaderAlarm.stopAlarm(context)
if (intervalInHours == 0 || !isEnabled)
return
// Get the time the alarm should fire the event to update.
val intervalInMillis = intervalInHours * 60 * 60 * 1000
val nextRun = SystemClock.elapsedRealtime() + intervalInMillis
// Start the alarm.
val pendingIntent = getPendingIntent(context)
context.alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
nextRun, intervalInMillis.toLong(), pendingIntent)
}
/**
* Stops the alarm if it's running.
* @param context the application context.
*/
fun stopAlarm(context: Context) {
val pendingIntent = getPendingIntent(context)
context.alarmManager.cancel(pendingIntent)
}
/**
* Returns broadcast intent
* @param context the application context.
* @return broadcast intent
*/
fun getPendingIntent(context: Context): PendingIntent {
return PendingIntent.getBroadcast(context, 0,
Intent(context, UpdateDownloaderAlarm::class.java).apply {
this.action = CHECK_UPDATE_ACTION
}, PendingIntent.FLAG_UPDATE_CURRENT)
}
}
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
// Start the alarm when the system is booted.
Intent.ACTION_BOOT_COMPLETED -> startAlarm(context)
// Update the library when the alarm fires an event.
CHECK_UPDATE_ACTION -> checkVersion(context)
}
}
fun checkVersion(context: Context) {
if (DeviceUtil.isNetworkConnected(context)) {
val updateChecker = GithubUpdateChecker(context)
updateChecker.checkForApplicationUpdate()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ release ->
//Get version of latest release
var newVersion = release.version
newVersion = newVersion.replace("[^\\d.]".toRegex(), "")
//Check if latest version is different from current version
if (newVersion != BuildConfig.VERSION_NAME) {
val downloadLink = release.downloadLink
val n = context.notification() {
setContentTitle(context.getString(R.string.update_check_notification_update_available))
addAction(android.R.drawable.stat_sys_download_done, context.getString(eu.kanade.tachiyomi.R.string.action_download),
UpdateDownloader(context).getInstallOnReceivedIntent(UpdateDownloader.InstallOnReceived.RETRY_DOWNLOAD, downloadLink))
setSmallIcon(android.R.drawable.stat_sys_download_done)
}
// Displays the progress bar on notification
context.notificationManager.notify(0, n);
}
}, {
it.printStackTrace()
})
}
}
}

View File

@ -5,7 +5,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
class LibraryMangaEvent(val mangas: Map<Int, List<Manga>>) { class LibraryMangaEvent(val mangas: Map<Int, List<Manga>>) {
fun getMangasForCategory(category: Category): List<Manga>? { fun getMangaForCategory(category: Category): List<Manga>? {
return mangas[category.id] return mangas[category.id]
} }
} }

View File

@ -57,7 +57,7 @@ class BackupFragment : BaseRxFragment<BackupPresenter>() {
restore_button.setOnClickListener { restore_button.setOnClickListener {
val intent = Intent(Intent.ACTION_GET_CONTENT) val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.addCategory(Intent.CATEGORY_OPENABLE) intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "application/octet-stream" intent.type = "application/*"
val chooser = Intent.createChooser(intent, getString(R.string.file_select_backup)) val chooser = Intent.createChooser(intent, getString(R.string.file_select_backup))
startActivityForResult(chooser, REQUEST_BACKUP_OPEN) startActivityForResult(chooser, REQUEST_BACKUP_OPEN)
} }
@ -76,7 +76,7 @@ class BackupFragment : BaseRxFragment<BackupPresenter>() {
fun onBackupCompleted(file: File) { fun onBackupCompleted(file: File) {
dismissBackupDialog() dismissBackupDialog()
val intent = Intent(Intent.ACTION_SEND) val intent = Intent(Intent.ACTION_SEND)
intent.type = "text/plain" intent.type = "application/json"
intent.putExtra(Intent.EXTRA_STREAM, Uri.parse("file://" + file)) intent.putExtra(Intent.EXTRA_STREAM, Uri.parse("file://" + file))
startActivity(Intent.createChooser(intent, "")) startActivity(Intent.createChooser(intent, ""))
} }

View File

@ -73,5 +73,4 @@ open class BaseActivity : AppCompatActivity() {
snack.f() snack.f()
snack.show() snack.show()
} }
} }

View File

@ -11,7 +11,6 @@ import nucleus.presenter.Presenter;
import nucleus.view.PresenterLifecycleDelegate; import nucleus.view.PresenterLifecycleDelegate;
import nucleus.view.ViewWithPresenter; import nucleus.view.ViewWithPresenter;
/** /**
* This class is an example of how an activity could controls it's presenter. * This class is an example of how an activity could controls it's presenter.
* You can inherit from this class or copy/paste this class's code to * You can inherit from this class or copy/paste this class's code to
@ -87,8 +86,9 @@ public abstract class BaseRxActivity<P extends Presenter> extends BaseActivity i
} }
@Override @Override
protected void onPause() { protected void onDestroy() {
super.onPause(); super.onDestroy();
presenterDelegate.onPause(isFinishing()); presenterDelegate.onDropView();
presenterDelegate.onDestroy(!isChangingConfigurations());
} }
} }

View File

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.ui.base.fragment; package eu.kanade.tachiyomi.ui.base.fragment;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.Fragment;
import eu.kanade.tachiyomi.App; import eu.kanade.tachiyomi.App;
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter; import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
@ -85,13 +84,14 @@ public abstract class BaseRxFragment<P extends Presenter> extends BaseFragment i
} }
@Override @Override
public void onPause() { public void onDestroyView() {
super.onPause(); super.onDestroyView();
presenterDelegate.onPause(getActivity().isFinishing() || isRemoving(this)); presenterDelegate.onDropView();
} }
private static boolean isRemoving(Fragment fragment) { @Override
Fragment parent = fragment.getParentFragment(); public void onDestroy() {
return fragment.isRemoving() || (parent != null && isRemoving(parent)); super.onDestroy();
presenterDelegate.onDestroy(!getActivity().isChangingConfigurations());
} }
} }

View File

@ -1,10 +1,14 @@
package eu.kanade.tachiyomi.ui.catalogue package eu.kanade.tachiyomi.ui.catalogue
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.RelativeLayout
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.util.inflate import eu.kanade.tachiyomi.util.inflate
import kotlinx.android.synthetic.main.fragment_catalogue.*
import kotlinx.android.synthetic.main.item_catalogue_grid.view.*
import java.util.* import java.util.*
/** /**
@ -68,6 +72,7 @@ class CatalogueAdapter(val fragment: CatalogueFragment) : FlexibleAdapter<Catalo
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CatalogueHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CatalogueHolder {
if (parent.id == R.id.catalogue_grid) { if (parent.id == R.id.catalogue_grid) {
val v = parent.inflate(R.layout.item_catalogue_grid) val v = parent.inflate(R.layout.item_catalogue_grid)
v.image_container.layoutParams = RelativeLayout.LayoutParams(MATCH_PARENT, coverHeight)
return CatalogueGridHolder(v, this, fragment) return CatalogueGridHolder(v, this, fragment)
} else { } else {
val v = parent.inflate(R.layout.item_catalogue_list) val v = parent.inflate(R.layout.item_catalogue_list)
@ -86,4 +91,10 @@ class CatalogueAdapter(val fragment: CatalogueFragment) : FlexibleAdapter<Catalo
holder.onSetValues(manga) holder.onSetValues(manga)
} }
/**
* Property to return the height for the covers based on the width to keep an aspect ratio.
*/
val coverHeight: Int
get() = fragment.catalogue_grid.itemWidth / 3 * 4
} }

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.ui.catalogue package eu.kanade.tachiyomi.ui.catalogue
import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.support.v7.widget.GridLayoutManager import android.support.v7.widget.GridLayoutManager
import android.support.v7.widget.SearchView import android.support.v7.widget.SearchView
@ -11,6 +12,7 @@ import android.widget.ArrayAdapter
import android.widget.ProgressBar import android.widget.ProgressBar
import android.widget.Spinner import android.widget.Spinner
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.f2prateek.rx.preferences.Preference
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
@ -31,7 +33,7 @@ import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.subjects.PublishSubject import rx.subjects.PublishSubject
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit.MILLISECONDS
/** /**
* Fragment that shows the manga from the catalogue. * Fragment that shows the manga from the catalogue.
@ -63,7 +65,8 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
/** /**
* Query of the search box. * Query of the search box.
*/ */
private var query = "" private val query: String?
get() = presenter.query
/** /**
* Selected index of the spinner (selected source). * Selected index of the spinner (selected source).
@ -85,6 +88,11 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
*/ */
private var queryDebouncerSubscription: Subscription? = null private var queryDebouncerSubscription: Subscription? = null
/**
* Subscription of the number of manga per row.
*/
private var numColumnsSubscription: Subscription? = null
/** /**
* Display mode of the catalogue (list or grid mode). * Display mode of the catalogue (list or grid mode).
*/ */
@ -102,23 +110,11 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
get() = (activity as MainActivity).toolbar get() = (activity as MainActivity).toolbar
companion object { companion object {
/**
* Key to save and restore [query] from a [Bundle].
*/
const val QUERY_KEY = "query_key"
/**
* Key to save and restore [selectedIndex] from a [Bundle].
*/
const val SELECTED_INDEX_KEY = "selected_index_key"
/** /**
* Creates a new instance of this fragment. * Creates a new instance of this fragment.
* *
* @return a new instance of [CatalogueFragment]. * @return a new instance of [CatalogueFragment].
*/ */
@JvmStatic
fun newInstance(): CatalogueFragment { fun newInstance(): CatalogueFragment {
return CatalogueFragment() return CatalogueFragment()
} }
@ -127,13 +123,6 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
setHasOptionsMenu(true) setHasOptionsMenu(true)
if (savedState != null) {
selectedIndex = savedState.getInt(SELECTED_INDEX_KEY)
query = savedState.getString(QUERY_KEY)
} else {
selectedIndex = presenter.getLastUsedSourceIndex()
}
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? {
@ -162,6 +151,12 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
switcher.showNext() switcher.showNext()
} }
numColumnsSubscription = getColumnsPreferenceForCurrentOrientation().asObservable()
.doOnNext { catalogue_grid.spanCount = it }
.skip(1)
// Set again the adapter to recalculate the covers height
.subscribe { catalogue_grid.adapter = adapter }
switcher.inAnimation = AnimationUtils.loadAnimation(activity, android.R.anim.fade_in) switcher.inAnimation = AnimationUtils.loadAnimation(activity, android.R.anim.fade_in)
switcher.outAnimation = AnimationUtils.loadAnimation(activity, android.R.anim.fade_out) switcher.outAnimation = AnimationUtils.loadAnimation(activity, android.R.anim.fade_out)
@ -175,19 +170,15 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
val onItemSelected = object : AdapterView.OnItemSelectedListener { val onItemSelected = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) { override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
val source = spinnerAdapter.getItem(position) val source = spinnerAdapter.getItem(position)
if (selectedIndex != position || adapter.isEmpty) {
// Set previous selection if it's not a valid source and notify the user
if (!presenter.isValidSource(source)) { if (!presenter.isValidSource(source)) {
spinner.setSelection(presenter.findFirstValidSource()) spinner.setSelection(selectedIndex)
context.toast(R.string.source_requires_login) context.toast(R.string.source_requires_login)
} else { } else if (source != presenter.source) {
selectedIndex = position selectedIndex = position
presenter.setEnabledSource(selectedIndex)
showProgressBar() showProgressBar()
glm.scrollToPositionWithOffset(0, 0) glm.scrollToPositionWithOffset(0, 0)
llm.scrollToPositionWithOffset(0, 0) llm.scrollToPositionWithOffset(0, 0)
presenter.startRequesting(source) presenter.setActiveSource(source)
}
} }
} }
@ -197,18 +188,15 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
spinner = Spinner(themedContext).apply { spinner = Spinner(themedContext).apply {
adapter = spinnerAdapter adapter = spinnerAdapter
selectedIndex = presenter.sources.indexOf(presenter.source)
setSelection(selectedIndex) setSelection(selectedIndex)
onItemSelectedListener = onItemSelected onItemSelectedListener = onItemSelected
} }
setToolbarTitle("") setToolbarTitle("")
toolbar.addView(spinner) toolbar.addView(spinner)
}
override fun onSaveInstanceState(outState: Bundle) { showProgressBar()
outState.putInt(SELECTED_INDEX_KEY, selectedIndex)
outState.putString(QUERY_KEY, query)
super.onSaveInstanceState(outState)
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@ -255,17 +243,20 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
return true return true
} }
override fun onStart() { override fun onResume() {
super.onStart() super.onResume()
initializeSearchSubscription() queryDebouncerSubscription = queryDebouncerSubject.debounce(SEARCH_TIMEOUT, MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { searchWithQuery(it) }
} }
override fun onStop() { override fun onPause() {
destroySearchSubscription() queryDebouncerSubscription?.unsubscribe()
super.onStop() super.onPause()
} }
override fun onDestroyView() { override fun onDestroyView() {
numColumnsSubscription?.unsubscribe()
searchItem?.let { searchItem?.let {
if (it.isActionViewExpanded) it.collapseActionView() if (it.isActionViewExpanded) it.collapseActionView()
} }
@ -274,51 +265,34 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
} }
/** /**
* Listen for query events on the debouncer. * Called when the input text changes or is submitted.
*/
private fun initializeSearchSubscription() {
queryDebouncerSubscription = queryDebouncerSubject.debounce(SEARCH_TIMEOUT, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { restartRequest(it) }
}
/**
* Unsubscribe from the query debouncer.
*/
private fun destroySearchSubscription() {
queryDebouncerSubscription?.unsubscribe()
}
/**
* Called when the input text changes or is submitted
* *
* @param query the new query. * @param query the new query.
* @param now whether to send the network call now or debounce it by [SEARCH_TIMEOUT]. * @param now whether to send the network call now or debounce it by [SEARCH_TIMEOUT].
*/ */
private fun onSearchEvent(query: String, now: Boolean) { private fun onSearchEvent(query: String, now: Boolean) {
if (now) { if (now) {
restartRequest(query) searchWithQuery(query)
} else { } else {
queryDebouncerSubject.onNext(query) queryDebouncerSubject.onNext(query)
} }
} }
/** /**
* Restarts the request. * Restarts the request with a new query.
* *
* @param newQuery the new query. * @param newQuery the new query.
*/ */
private fun restartRequest(newQuery: String) { private fun searchWithQuery(newQuery: String) {
// If text didn't change, do nothing // If text didn't change, do nothing
if (query == newQuery) if (query == newQuery)
return return
query = newQuery
showProgressBar() showProgressBar()
catalogue_grid.layoutManager.scrollToPosition(0) catalogue_grid.layoutManager.scrollToPosition(0)
catalogue_list.layoutManager.scrollToPosition(0) catalogue_list.layoutManager.scrollToPosition(0)
presenter.restartRequest(query) presenter.restartPager(newQuery)
} }
/** /**
@ -359,7 +333,7 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
catalogue_view.snack(error.message ?: "") { catalogue_view.snack(error.message ?: "") {
setAction(R.string.action_retry) { setAction(R.string.action_retry) {
showProgressBar() showProgressBar()
presenter.retryRequest() presenter.retryPage()
} }
} }
} }
@ -391,6 +365,18 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
} }
} }
/**
* Returns a preference for the number of manga per row based on the current orientation.
*
* @return the preference.
*/
fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> {
return if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT)
presenter.prefs.portraitColumns()
else
presenter.prefs.landscapeColumns()
}
/** /**
* Returns the view holder for the given manga. * Returns the view holder for the given manga.
* *
@ -432,8 +418,7 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
override fun onListItemClick(position: Int): Boolean { override fun onListItemClick(position: Int): Boolean {
val item = adapter.getItem(position) ?: return false val item = adapter.getItem(position) ?: return false
val intent = MangaActivity.newIntent(activity, item) val intent = MangaActivity.newIntent(activity, item, true)
intent.putExtra(MangaActivity.FROM_CATALOGUE, true)
startActivity(intent) startActivity(intent)
return false return false
} }
@ -444,16 +429,16 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
* @param position the position of the element clicked. * @param position the position of the element clicked.
*/ */
override fun onListItemLongClick(position: Int) { override fun onListItemLongClick(position: Int) {
val selectedManga = adapter.getItem(position) val manga = adapter.getItem(position) ?: return
val textRes = if (selectedManga.favorite) R.string.remove_from_library else R.string.add_to_library val textRes = if (manga.favorite) R.string.remove_from_library else R.string.add_to_library
MaterialDialog.Builder(activity) MaterialDialog.Builder(activity)
.items(getString(textRes)) .items(getString(textRes))
.itemsCallback { dialog, itemView, which, text -> .itemsCallback { dialog, itemView, which, text ->
when (which) { when (which) {
0 -> { 0 -> {
presenter.changeMangaFavorite(selectedManga) presenter.changeMangaFavorite(manga)
adapter.notifyItemChanged(position) adapter.notifyItemChanged(position)
} }
} }

View File

@ -5,6 +5,7 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.source.EN
import eu.kanade.tachiyomi.data.source.SourceManager import eu.kanade.tachiyomi.data.source.SourceManager
import eu.kanade.tachiyomi.data.source.base.Source import eu.kanade.tachiyomi.data.source.base.Source
import eu.kanade.tachiyomi.data.source.model.MangasPage import eu.kanade.tachiyomi.data.source.model.MangasPage
@ -51,12 +52,13 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
/** /**
* Query from the view. * Query from the view.
*/ */
private var query: String? = null var query = ""
private set
/** /**
* Pager containing a list of manga results. * Pager containing a list of manga results.
*/ */
private lateinit var pager: RxPager<Manga> private var pager = RxPager<Manga>()
/** /**
* Last fetched page from network. * Last fetched page from network.
@ -76,45 +78,36 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
companion object { companion object {
/** /**
* Id of the restartable that delivers a list of manga from network. * Id of the restartable that delivers a list of manga.
*/ */
const val GET_MANGA_LIST = 1 const val PAGER = 1
/** /**
* Id of the restartable that requests the list of manga from network. * Id of the restartable that requests a page of manga from network.
*/ */
const val GET_MANGA_PAGE = 2 const val REQUEST_PAGE = 2
/** /**
* Id of the restartable that initializes the details of a manga. * Id of the restartable that initializes the details of manga.
*/ */
const val GET_MANGA_DETAIL = 3 const val GET_MANGA_DETAILS = 3
/** /**
* Key to save and restore [source] from a [Bundle]. * Key to save and restore [query] from a [Bundle].
*/ */
const val ACTIVE_SOURCE_KEY = "active_source" const val QUERY_KEY = "query_key"
} }
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
source = getLastUsedSource()
if (savedState != null) { if (savedState != null) {
source = sourceManager.get(savedState.getInt(ACTIVE_SOURCE_KEY))!! query = savedState.getString(QUERY_KEY, "")
} }
pager = RxPager() startableLatestCache(GET_MANGA_DETAILS,
startableReplay(GET_MANGA_LIST,
{ pager.results() },
{ view, pair -> view.onAddPage(pair.first, pair.second) })
startableFirst(GET_MANGA_PAGE,
{ pager.request { page -> getMangasPageObservable(page + 1) } },
{ view, next -> },
{ view, error -> view.onAddPageError(error) })
startableLatestCache(GET_MANGA_DETAIL,
{ mangaDetailSubject.observeOn(Schedulers.io()) { mangaDetailSubject.observeOn(Schedulers.io())
.flatMap { Observable.from(it) } .flatMap { Observable.from(it) }
.filter { !it.initialized } .filter { !it.initialized }
@ -126,10 +119,22 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
add(prefs.catalogueAsList().asObservable() add(prefs.catalogueAsList().asObservable()
.subscribe { setDisplayMode(it) }) .subscribe { setDisplayMode(it) })
startableReplay(PAGER,
{ pager.results() },
{ view, pair -> view.onAddPage(pair.first, pair.second) })
startableFirst(REQUEST_PAGE,
{ pager.request { page -> getMangasPageObservable(page + 1) } },
{ view, next -> },
{ view, error -> view.onAddPageError(error) })
start(PAGER)
start(REQUEST_PAGE)
} }
override fun onSave(state: Bundle) { override fun onSave(state: Bundle) {
state.putInt(ACTIVE_SOURCE_KEY, source.id) state.putString(QUERY_KEY, query)
super.onSave(state) super.onSave(state)
} }
@ -141,37 +146,38 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
private fun setDisplayMode(asList: Boolean) { private fun setDisplayMode(asList: Boolean) {
isListMode = asList isListMode = asList
if (asList) { if (asList) {
stop(GET_MANGA_DETAIL) stop(GET_MANGA_DETAILS)
} else { } else {
start(GET_MANGA_DETAIL) start(GET_MANGA_DETAILS)
} }
} }
/** /**
* Starts the request with the given source. * Sets the active source and restarts the pager.
* *
* @param source the active source. * @param source the new active source.
*/ */
fun startRequesting(source: Source) { fun setActiveSource(source: Source) {
prefs.lastUsedCatalogueSource().set(source.id)
this.source = source this.source = source
restartRequest(null) restartPager()
} }
/** /**
* Restarts the request for the active source with a query. * Restarts the request for the active source.
* *
* @param query a query, or null if searching popular manga. * @param query the query, or null if searching popular manga.
*/ */
fun restartRequest(query: String?) { fun restartPager(query: String = "") {
this.query = query this.query = query
stop(GET_MANGA_PAGE) stop(REQUEST_PAGE)
lastMangasPage = null lastMangasPage = null
if (!isListMode) { if (!isListMode) {
start(GET_MANGA_DETAIL) start(GET_MANGA_DETAILS)
} }
start(GET_MANGA_LIST) start(PAGER)
start(GET_MANGA_PAGE) start(REQUEST_PAGE)
} }
/** /**
@ -179,15 +185,22 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
*/ */
fun requestNext() { fun requestNext() {
if (hasNextPage()) { if (hasNextPage()) {
start(GET_MANGA_PAGE) start(REQUEST_PAGE)
} }
} }
/** /**
* Retry a failed request. * Returns true if the last fetched page has a next page.
*/ */
fun retryRequest() { fun hasNextPage(): Boolean {
start(GET_MANGA_PAGE) return lastMangasPage?.nextPageUrl != null
}
/**
* Retries the current request that failed.
*/
fun retryPage() {
start(REQUEST_PAGE)
} }
/** /**
@ -202,12 +215,12 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
nextMangasPage.url = lastMangasPage!!.nextPageUrl nextMangasPage.url = lastMangasPage!!.nextPageUrl
} }
val obs = if (query.isNullOrEmpty()) val observable = if (query.isEmpty())
source.pullPopularMangasFromNetwork(nextMangasPage) source.pullPopularMangasFromNetwork(nextMangasPage)
else else
source.searchMangasFromNetwork(nextMangasPage, query!!) source.searchMangasFromNetwork(nextMangasPage, query)
return obs.subscribeOn(Schedulers.io()) return observable.subscribeOn(Schedulers.io())
.doOnNext { lastMangasPage = it } .doOnNext { lastMangasPage = it }
.flatMap { Observable.from(it.mangas) } .flatMap { Observable.from(it.mangas) }
.map { networkToLocalManga(it) } .map { networkToLocalManga(it) }
@ -259,23 +272,17 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
} }
/** /**
* Returns true if the last fetched page has a next page. * Returns the last used source from preferences or the first valid source.
*/
fun hasNextPage(): Boolean {
return lastMangasPage?.nextPageUrl != null
}
/**
* Gets the last used source from preferences, or the first valid source.
* *
* @return the index of the last used source. * @return a source.
*/ */
fun getLastUsedSourceIndex(): Int { fun getLastUsedSource(): Source {
val index = prefs.lastUsedCatalogueSource().get() ?: -1 val id = prefs.lastUsedCatalogueSource().get() ?: -1
if (index < 0 || index >= sources.size || !isValidSource(sources[index])) { val source = sourceManager.get(id)
if (!isValidSource(source)) {
return findFirstValidSource() return findFirstValidSource()
} }
return index return source!!
} }
/** /**
@ -284,11 +291,16 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
* @param source the source to check. * @param source the source to check.
* @return true if the source is valid, false otherwise. * @return true if the source is valid, false otherwise.
*/ */
fun isValidSource(source: Source): Boolean = with(source) { fun isValidSource(source: Source?): Boolean {
if (!isLoginRequired || isLogged) if (source == null) return false
return true
prefs.getSourceUsername(this) != "" && prefs.getSourcePassword(this) != "" return with(source) {
if (!isLoginRequired || isLogged) {
true
} else {
prefs.sourceUsername(this) != "" && prefs.sourcePassword(this) != ""
}
}
} }
/** /**
@ -296,17 +308,8 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
* *
* @return the index of the first valid source. * @return the index of the first valid source.
*/ */
fun findFirstValidSource(): Int { fun findFirstValidSource(): Source {
return sources.indexOfFirst { isValidSource(it) } return sources.find { isValidSource(it) }!!
}
/**
* Sets the enabled source.
*
* @param index the index of the source in [sources].
*/
fun setEnabledSource(index: Int) {
prefs.lastUsedCatalogueSource().set(index)
} }
/** /**
@ -317,7 +320,7 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
// Ensure at least one language // Ensure at least one language
if (languages.isEmpty()) { if (languages.isEmpty()) {
languages.add("EN") languages.add(EN.code)
} }
return sourceManager.getSources() return sourceManager.getSources()

View File

@ -10,7 +10,13 @@ import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.widget.NpaLinearLayoutManager import eu.kanade.tachiyomi.widget.NpaLinearLayoutManager
import kotlinx.android.synthetic.main.fragment_download_queue.* import kotlinx.android.synthetic.main.fragment_download_queue.*
import nucleus.factory.RequiresPresenter import nucleus.factory.RequiresPresenter
import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import rx.subscriptions.CompositeSubscription
import java.util.*
import java.util.concurrent.TimeUnit
/** /**
* Fragment that shows the currently active downloads. * Fragment that shows the currently active downloads.
@ -40,9 +46,14 @@ class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
private var clearButton: MenuItem? = null private var clearButton: MenuItem? = null
/** /**
* Subscription to know if the download queue is running. * Subscription list to be cleared during [onPause].
*/ */
private var queueStatusSubscription: Subscription? = null private val resumeSubscriptions by lazy { CompositeSubscription() }
/**
* Map of subscriptions for active downloads.
*/
private val progressSubscriptions by lazy { HashMap<Download, Subscription>() }
/** /**
* Whether the download queue is running or not. * Whether the download queue is running or not.
@ -66,8 +77,7 @@ class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
setHasOptionsMenu(true) setHasOptionsMenu(true)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View {
savedState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_download_queue, container, false) return inflater.inflate(R.layout.fragment_download_queue, container, false)
} }
@ -123,15 +133,91 @@ class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
queueStatusSubscription = presenter.downloadManager.runningSubject presenter.downloadManager.runningSubject
.observeOn(AndroidSchedulers.mainThread())
.subscribe { onQueueStatusChange(it) } .subscribe { onQueueStatusChange(it) }
.apply { resumeSubscriptions.add(this) }
presenter.getStatusObservable()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { onStatusChange(it) }
.apply { resumeSubscriptions.add(this) }
presenter.getProgressObservable()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { onUpdateDownloadedPages(it) }
.apply { resumeSubscriptions.add(this) }
} }
override fun onPause() { override fun onPause() {
queueStatusSubscription?.unsubscribe() for (subscription in progressSubscriptions.values) {
subscription.unsubscribe()
}
progressSubscriptions.clear()
resumeSubscriptions.clear()
super.onPause() super.onPause()
} }
/**
* Called when the status of a download changes.
*
* @param download the download whose status has changed.
*/
private fun onStatusChange(download: Download) {
when (download.status) {
Download.DOWNLOADING -> {
observeProgress(download)
// Initial update of the downloaded pages
onUpdateDownloadedPages(download)
}
Download.DOWNLOADED -> {
unsubscribeProgress(download)
onUpdateProgress(download)
onUpdateDownloadedPages(download)
}
Download.ERROR -> unsubscribeProgress(download)
}
}
/**
* Observe the progress of a download and notify the view.
*
* @param download the download to observe its progress.
*/
private fun observeProgress(download: Download) {
val subscription = Observable.interval(50, TimeUnit.MILLISECONDS, Schedulers.newThread())
// Get the sum of percentages for all the pages.
.flatMap {
Observable.from(download.pages)
.map { it.progress }
.reduce { x, y -> x + y }
}
// Keep only the latest emission to avoid backpressure.
.onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { progress ->
// Update the view only if the progress has changed.
if (download.totalProgress != progress) {
download.totalProgress = progress
onUpdateProgress(download)
}
}
// Avoid leaking subscriptions
progressSubscriptions.remove(download)?.unsubscribe()
progressSubscriptions.put(download, subscription)
}
/**
* Unsubscribes the given download from the progress subscriptions.
*
* @param download the download to unsubscribe.
*/
private fun unsubscribeProgress(download: Download) {
progressSubscriptions.remove(download)?.unsubscribe()
}
/** /**
* Called when the queue's status has changed. Updates the visibility of the buttons. * Called when the queue's status has changed. Updates the visibility of the buttons.
* *
@ -157,16 +243,16 @@ class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
} }
/** /**
* Called from the presenter when the status of a download changes. * Called when the progress of a download changes.
* *
* @param download the download whose status has changed. * @param download the download whose progress has changed.
*/ */
fun onUpdateProgress(download: Download) { fun onUpdateProgress(download: Download) {
getHolder(download)?.notifyProgress() getHolder(download)?.notifyProgress()
} }
/** /**
* Called from the presenter when a page of a download is downloaded. * Called when a page of a download is downloaded.
* *
* @param download the download whose page has been downloaded. * @param download the download whose page has been downloaded.
*/ */

View File

@ -10,7 +10,7 @@ import kotlinx.android.synthetic.main.item_download.view.*
* All the elements from the layout file "item_download" are available in this class. * All the elements from the layout file "item_download" are available in this class.
* *
* @param view the inflated view for this holder. * @param view the inflated view for this holder.
* @constructor creates a new library holder. * @constructor creates a new download holder.
*/ */
class DownloadHolder(private val view: View) : RecyclerView.ViewHolder(view) { class DownloadHolder(private val view: View) : RecyclerView.ViewHolder(view) {

View File

@ -6,12 +6,7 @@ import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.download.model.DownloadQueue import eu.kanade.tachiyomi.data.download.model.DownloadQueue
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import rx.Observable import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import timber.log.Timber import timber.log.Timber
import java.util.*
import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -19,6 +14,13 @@ import javax.inject.Inject
*/ */
class DownloadPresenter : BasePresenter<DownloadFragment>() { class DownloadPresenter : BasePresenter<DownloadFragment>() {
companion object {
/**
* Id of the restartable that returns the download queue.
*/
const val GET_DOWNLOAD_QUEUE = 1
}
/** /**
* Download manager. * Download manager.
*/ */
@ -30,28 +32,6 @@ class DownloadPresenter : BasePresenter<DownloadFragment>() {
val downloadQueue: DownloadQueue val downloadQueue: DownloadQueue
get() = downloadManager.queue get() = downloadManager.queue
/**
* Map of subscriptions for active downloads.
*/
private val progressSubscriptions by lazy { HashMap<Download, Subscription>() }
/**
* Subscription for status changes on downloads.
*/
private var statusSubscription: Subscription? = null
/**
* Subscription for downloaded pages for active downloads.
*/
private var pageProgressSubscription: Subscription? = null
companion object {
/**
* Id of the restartable that returns the download queue.
*/
const val GET_DOWNLOAD_QUEUE = 1
}
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
@ -65,102 +45,14 @@ class DownloadPresenter : BasePresenter<DownloadFragment>() {
} }
} }
override fun onTakeView(view: DownloadFragment) { fun getStatusObservable(): Observable<Download> {
super.onTakeView(view) return downloadQueue.getStatusObservable()
statusSubscription = downloadQueue.getStatusObservable()
.startWith(downloadQueue.getActiveDownloads()) .startWith(downloadQueue.getActiveDownloads())
.observeOn(AndroidSchedulers.mainThread()) }
.subscribe { processStatus(it, view) }
add(statusSubscription) fun getProgressObservable(): Observable<Download> {
return downloadQueue.getProgressObservable()
pageProgressSubscription = downloadQueue.getProgressObservable()
.onBackpressureBuffer() .onBackpressureBuffer()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { view.onUpdateDownloadedPages(it) }
add(pageProgressSubscription)
}
override fun onDropView() {
destroySubscriptions()
super.onDropView()
}
/**
* Process the status of a download when its status has changed and notify the view.
*
* @param download the download whose status has changed.
* @param view the view.
*/
private fun processStatus(download: Download, view: DownloadFragment) {
when (download.status) {
Download.DOWNLOADING -> {
observeProgress(download, view)
// Initial update of the downloaded pages
view.onUpdateDownloadedPages(download)
}
Download.DOWNLOADED -> {
unsubscribeProgress(download)
view.onUpdateProgress(download)
view.onUpdateDownloadedPages(download)
}
Download.ERROR -> unsubscribeProgress(download)
}
}
/**
* Observe the progress of a download and notify the view.
*
* @param download the download to observe its progress.
* @param view the view.
*/
private fun observeProgress(download: Download, view: DownloadFragment) {
val subscription = Observable.interval(50, TimeUnit.MILLISECONDS, Schedulers.newThread())
// Get the sum of percentages for all the pages.
.flatMap {
Observable.from(download.pages)
.map { it.progress }
.reduce { x, y -> x + y }
}
// Keep only the latest emission to avoid backpressure.
.onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { progress ->
// Update the view only if the progress has changed.
if (download.totalProgress != progress) {
download.totalProgress = progress
view.onUpdateProgress(download)
}
}
// Avoid leaking subscriptions
progressSubscriptions.remove(download)?.unsubscribe()
progressSubscriptions.put(download, subscription)
}
/**
* Unsubscribes the given download from the progress subscriptions.
*
* @param download the download to unsubscribe.
*/
private fun unsubscribeProgress(download: Download) {
progressSubscriptions.remove(download)?.unsubscribe()
}
/**
* Destroys all the subscriptions of the presenter.
*/
private fun destroySubscriptions() {
for (subscription in progressSubscriptions.values) {
subscription.unsubscribe()
}
progressSubscriptions.clear()
remove(pageProgressSubscription)
remove(statusSubscription)
} }
/** /**

View File

@ -15,7 +15,6 @@ import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment
import eu.kanade.tachiyomi.ui.manga.MangaActivity import eu.kanade.tachiyomi.ui.manga.MangaActivity
import kotlinx.android.synthetic.main.fragment_library_category.* import kotlinx.android.synthetic.main.fragment_library_category.*
import rx.Subscription import rx.Subscription
import java.util.*
/** /**
* Fragment containing the library manga for a certain category. * Fragment containing the library manga for a certain category.
@ -34,14 +33,6 @@ class LibraryCategoryFragment : BaseFragment(), FlexibleViewHolder.OnListItemCli
*/ */
private var position: Int = 0 private var position: Int = 0
/**
* Manga in this category.
*/
private var mangas: List<Manga>? = null
set(value) {
field = value ?: ArrayList()
}
/** /**
* Subscription for the library manga. * Subscription for the library manga.
*/ */
@ -119,7 +110,7 @@ class LibraryCategoryFragment : BaseFragment(), FlexibleViewHolder.OnListItemCli
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
libraryMangaSubscription = libraryPresenter.libraryMangaSubject libraryMangaSubscription = libraryPresenter.libraryMangaSubject
.subscribe { if (it != null) onNextLibraryManga(it) } .subscribe { onNextLibraryManga(it) }
} }
override fun onPause() { override fun onPause() {
@ -134,8 +125,8 @@ class LibraryCategoryFragment : BaseFragment(), FlexibleViewHolder.OnListItemCli
} }
/** /**
* Subscribe to [LibraryMangaEvent]. When an event is received, it updates [mangas] if needed * Subscribe to [LibraryMangaEvent]. When an event is received, it updates the content of the
* and refresh the content of the adapter. * adapter.
* *
* @param event the event received. * @param event the event received.
*/ */
@ -146,15 +137,11 @@ class LibraryCategoryFragment : BaseFragment(), FlexibleViewHolder.OnListItemCli
// When a category is deleted, the index can be greater than the number of categories. // When a category is deleted, the index can be greater than the number of categories.
if (position >= categories.size) return if (position >= categories.size) return
// Get the manga list for this category // Get the manga list for this category.
val mangaForCategory = event.getMangasForCategory(categories[position]) val mangaForCategory = event.getMangaForCategory(categories[position]) ?: emptyList()
// Update the list only if the reference to the list is different, avoiding reseting the // Update the category with its manga.
// adapter after every onResume. adapter.setItems(mangaForCategory)
if (mangas !== mangaForCategory) {
mangas = mangaForCategory
adapter.setItems(mangas ?: emptyList())
}
} }
/** /**

View File

@ -125,6 +125,11 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
} }
} }
override fun onResume() {
super.onResume()
presenter.subscribeLibrary()
}
override fun onDestroyView() { override fun onDestroyView() {
tabs.visibility = View.GONE tabs.visibility = View.GONE
super.onDestroyView() super.onDestroyView()
@ -385,10 +390,15 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
* @param mangas the manga list to move. * @param mangas the manga list to move.
*/ */
private fun moveMangasToCategories(mangas: List<Manga>) { private fun moveMangasToCategories(mangas: List<Manga>) {
val categories = presenter.categories
val commonCategoriesIndexes = presenter.getCommonCategories(mangas)
.map { categories.indexOf(it) }
.toTypedArray()
MaterialDialog.Builder(activity) MaterialDialog.Builder(activity)
.title(R.string.action_move_category) .title(R.string.action_move_category)
.items(presenter.getCategoryNames()) .items(categories.map { it.name })
.itemsCallbackMultiChoice(null) { dialog, positions, text -> .itemsCallbackMultiChoice(commonCategoriesIndexes) { dialog, positions, text ->
presenter.moveMangasToCategories(positions, mangas) presenter.moveMangasToCategories(positions, mangas)
destroyActionModeIfNeeded() destroyActionModeIfNeeded()
true true

View File

@ -44,7 +44,7 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
/** /**
* Subject to notify the library's viewpager for updates. * Subject to notify the library's viewpager for updates.
*/ */
val libraryMangaSubject = BehaviorSubject.create<LibraryMangaEvent?>() val libraryMangaSubject = BehaviorSubject.create<LibraryMangaEvent>()
/** /**
* Database. * Database.
@ -91,18 +91,6 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
} }
override fun onDropView() {
libraryMangaSubject.onNext(null)
super.onDropView()
}
override fun onTakeView(libraryFragment: LibraryFragment) {
super.onTakeView(libraryFragment)
if (isUnsubscribed(GET_LIBRARY)) {
start(GET_LIBRARY)
}
}
/** /**
* Get the categories and all its manga from the database. * Get the categories and all its manga from the database.
* *
@ -114,13 +102,6 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
} }
/**
* Update the library information
*/
fun updateLibrary() {
start(GET_LIBRARY)
}
/** /**
* Get the categories from the database. * Get the categories from the database.
* *
@ -151,6 +132,22 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
} }
} }
/**
* Resubscribes to library if needed.
*/
fun subscribeLibrary() {
if (isUnsubscribed(GET_LIBRARY)) {
start(GET_LIBRARY)
}
}
/**
* Resubscribes to library.
*/
fun updateLibrary() {
start(GET_LIBRARY)
}
/** /**
* Filter library by preference * Filter library by preference
* *
@ -220,11 +217,13 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
} }
/** /**
* Get the category names as a list. * Returns the common categories for the given list of manga.
*
* @param mangas the list of manga.
*/ */
fun getCategoryNames(): List<String> { fun getCommonCategories(mangas: List<Manga>) = mangas.toSet()
return categories.map { it.name } .map { db.getCategoriesForManga(it).executeAsBlocking() }
} .reduce { set1: Iterable<Category>, set2 -> set1.intersect(set2) }
/** /**
* Remove the selected manga from the library. * Remove the selected manga from the library.

View File

@ -39,7 +39,7 @@ class MainActivity : BaseActivity() {
supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_menu_white_24dp) supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_menu_white_24dp)
if (Build.VERSION.SDK_INT >= 21) { if (Build.VERSION.SDK_INT >= 21) {
window.statusBarColor = android.R.color.transparent; window.statusBarColor = android.R.color.transparent
} }
// Set behavior of Navigation drawer // Set behavior of Navigation drawer
@ -73,6 +73,16 @@ class MainActivity : BaseActivity() {
return true return true
} }
override fun onBackPressed() {
supportFragmentManager.findFragmentById(R.id.frame_container)?.let {
if (it !is LibraryFragment) {
setFragment(LibraryFragment.newInstance())
} else {
super.onBackPressed()
}
} ?: super.onBackPressed()
}
fun setFragment(fragment: Fragment) { fun setFragment(fragment: Fragment) {
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.replace(R.id.frame_container, fragment) .replace(R.id.frame_container, fragment)

View File

@ -23,21 +23,24 @@ class MangaActivity : BaseRxActivity<MangaPresenter>() {
companion object { companion object {
val FROM_CATALOGUE = "from_catalogue" const val FROM_CATALOGUE_EXTRA = "from_catalogue"
val INFO_FRAGMENT = 0 const val MANGA_EXTRA = "manga"
val CHAPTERS_FRAGMENT = 1 const val INFO_FRAGMENT = 0
val MYANIMELIST_FRAGMENT = 2 const val CHAPTERS_FRAGMENT = 1
const val MYANIMELIST_FRAGMENT = 2
fun newIntent(context: Context, manga: Manga): Intent { fun newIntent(context: Context, manga: Manga, fromCatalogue: Boolean = false): Intent {
val intent = Intent(context, MangaActivity::class.java)
SharedData.put(MangaEvent(manga)) SharedData.put(MangaEvent(manga))
return intent return Intent(context, MangaActivity::class.java).apply {
putExtra(FROM_CATALOGUE_EXTRA, fromCatalogue)
putExtra(MANGA_EXTRA, manga.id)
}
} }
} }
private lateinit var adapter: MangaDetailAdapter private lateinit var adapter: MangaDetailAdapter
var isCatalogueManga: Boolean = false var fromCatalogue: Boolean = false
private set private set
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
@ -45,16 +48,21 @@ class MangaActivity : BaseRxActivity<MangaPresenter>() {
super.onCreate(savedState) super.onCreate(savedState)
setContentView(R.layout.activity_manga) setContentView(R.layout.activity_manga)
presenter.setMangaEvent(SharedData.getOrPut(MangaEvent::class.java) {
val id = intent.getLongExtra(MANGA_EXTRA, 0)
MangaEvent(presenter.db.getManga(id).executeAsBlocking()!!)
})
setupToolbar(toolbar) setupToolbar(toolbar)
isCatalogueManga = intent.getBooleanExtra(FROM_CATALOGUE, false) fromCatalogue = intent.getBooleanExtra(FROM_CATALOGUE_EXTRA, false)
adapter = MangaDetailAdapter(supportFragmentManager, this) adapter = MangaDetailAdapter(supportFragmentManager, this)
view_pager.adapter = adapter view_pager.adapter = adapter
tabs.setupWithViewPager(view_pager) tabs.setupWithViewPager(view_pager)
if (!isCatalogueManga) if (!fromCatalogue)
view_pager.currentItem = CHAPTERS_FRAGMENT view_pager.currentItem = CHAPTERS_FRAGMENT
requestPermissionsOnMarshmallow() requestPermissionsOnMarshmallow()
@ -72,7 +80,7 @@ class MangaActivity : BaseRxActivity<MangaPresenter>() {
init { init {
pageCount = 2 pageCount = 2
if (!activity.isCatalogueManga && activity.presenter.syncManager.myAnimeList.isLogged) if (!activity.fromCatalogue && activity.presenter.syncManager.myAnimeList.isLogged)
pageCount++ pageCount++
} }

View File

@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.event.MangaEvent
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.SharedData import eu.kanade.tachiyomi.util.SharedData
import rx.Observable import rx.Observable
import rx.Subscription
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -31,37 +32,21 @@ class MangaPresenter : BasePresenter<MangaActivity>() {
*/ */
lateinit var manga: Manga lateinit var manga: Manga
/** var mangaSubscription: Subscription? = null
* Key to save and restore [manga] from a bundle.
*/
private val MANGA_KEY = "manga_key"
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
if (savedState == null) {
manga = SharedData.get(MangaEvent::class.java)!!.manga
} else {
manga = savedState.getSerializable(MANGA_KEY) as Manga
SharedData.put(MangaEvent(manga))
}
// Prepare a subject to communicate the chapters and info presenters for the chapter count. // Prepare a subject to communicate the chapters and info presenters for the chapter count.
SharedData.put(ChapterCountEvent()) SharedData.put(ChapterCountEvent())
}
Observable.just(manga) fun setMangaEvent(event: MangaEvent) {
if (isUnsubscribed(mangaSubscription)) {
manga = event.manga
mangaSubscription = Observable.just(manga)
.subscribeLatestCache({ view, manga -> view.onSetManga(manga) }) .subscribeLatestCache({ view, manga -> view.onSetManga(manga) })
} }
override fun onDestroy() {
SharedData.remove(MangaEvent::class.java)
SharedData.remove(ChapterCountEvent::class.java)
super.onDestroy()
}
override fun onSave(state: Bundle) {
state.putSerializable(MANGA_KEY, manga)
super.onSave(state)
} }
} }

View File

@ -181,11 +181,10 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
} }
val isCatalogueManga: Boolean val isCatalogueManga: Boolean
get() = (activity as MangaActivity).isCatalogueManga get() = (activity as MangaActivity).fromCatalogue
fun openChapter(chapter: Chapter, hasAnimation: Boolean = false) { fun openChapter(chapter: Chapter, hasAnimation: Boolean = false) {
presenter.onOpenChapter(chapter) val intent = ReaderActivity.newIntent(activity, presenter.manga, chapter)
val intent = ReaderActivity.newIntent(activity)
if (hasAnimation) { if (hasAnimation) {
intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
} }

View File

@ -159,10 +159,6 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
refreshChapters() refreshChapters()
} }
fun onOpenChapter(chapter: Chapter) {
SharedData.put(ReaderEvent(manga, chapter))
}
fun getNextUnreadChapter(): Chapter? { fun getNextUnreadChapter(): Chapter? {
return db.getNextUnreadChapter(manga).executeAsBlocking() return db.getNextUnreadChapter(manga).executeAsBlocking()
} }

View File

@ -98,7 +98,8 @@ class MangaInfoFragment : BaseRxFragment<MangaInfoPresenter>() {
val coverCache = presenter.coverCache val coverCache = presenter.coverCache
val headers = presenter.source.glideHeaders val headers = presenter.source.glideHeaders
// Check if thumbnail_url is given. // Set cover if it wasn't already.
if (manga_cover.drawable == null) {
manga.thumbnail_url?.let { url -> manga.thumbnail_url?.let { url ->
if (manga.favorite) { if (manga.favorite) {
coverCache.saveOrLoadFromCache(url, headers) { coverCache.saveOrLoadFromCache(url, headers) {
@ -133,6 +134,7 @@ class MangaInfoFragment : BaseRxFragment<MangaInfoPresenter>() {
} }
} }
} }
}
/** /**
* Update chapter count TextView. * Update chapter count TextView.

View File

@ -91,9 +91,7 @@ class MyAnimeListDialogFragment : DialogFragment() {
} }
private fun onPositiveButtonClick() { private fun onPositiveButtonClick() {
selectedItem?.let { presenter.registerManga(selectedItem)
presenter.registerManga(it)
}
} }
private fun search(query: String) { private fun search(query: String) {

View File

@ -56,6 +56,10 @@ class MyAnimeListFragment : BaseRxFragment<MyAnimeListPresenter>() {
myanimelist_status.text = presenter.myAnimeList.getStatus(it.status) myanimelist_status.text = presenter.myAnimeList.getStatus(it.status)
} ?: run { } ?: run {
myanimelist_title.setTextAppearance(context, R.style.TextAppearance_Medium_Button) myanimelist_title.setTextAppearance(context, R.style.TextAppearance_Medium_Button)
myanimelist_title.setText(R.string.action_edit)
myanimelist_chapters.text = ""
myanimelist_score.text = ""
myanimelist_status.text = ""
} }
} }

View File

@ -123,7 +123,8 @@ class MyAnimeListPresenter : BasePresenter<MyAnimeListFragment>() {
stop(GET_SEARCH_RESULTS) stop(GET_SEARCH_RESULTS)
} }
fun registerManga(sync: MangaSync) { fun registerManga(sync: MangaSync?) {
if (sync != null) {
sync.manga_id = manga.id sync.manga_id = manga.id
add(myAnimeList.bind(sync) add(myAnimeList.bind(sync)
.flatMap { response -> .flatMap { response ->
@ -137,6 +138,9 @@ class MyAnimeListPresenter : BasePresenter<MyAnimeListFragment>() {
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe({ }, .subscribe({ },
{ error -> context.toast(error.message) })) { error -> context.toast(error.message) }))
} else {
db.deleteMangaSyncForManga(manga).executeAsBlocking()
}
} }
fun getAllStatus(): List<String> { fun getAllStatus(): List<String> {

View File

@ -21,6 +21,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.source.model.Page import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.event.ReaderEvent
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
import eu.kanade.tachiyomi.ui.base.listener.SimpleAnimationListener import eu.kanade.tachiyomi.ui.base.listener.SimpleAnimationListener
import eu.kanade.tachiyomi.ui.base.listener.SimpleSeekBarListener import eu.kanade.tachiyomi.ui.base.listener.SimpleSeekBarListener
@ -30,6 +31,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.RightToLeftReader
import eu.kanade.tachiyomi.ui.reader.viewer.pager.vertical.VerticalReader import eu.kanade.tachiyomi.ui.reader.viewer.pager.vertical.VerticalReader
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonReader import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonReader
import eu.kanade.tachiyomi.util.GLUtil import eu.kanade.tachiyomi.util.GLUtil
import eu.kanade.tachiyomi.util.SharedData
import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.util.toast
import kotlinx.android.synthetic.main.activity_reader.* import kotlinx.android.synthetic.main.activity_reader.*
import kotlinx.android.synthetic.main.reader_menu.* import kotlinx.android.synthetic.main.reader_menu.*
@ -53,7 +55,8 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
const val MENU_VISIBLE = "menu_visible" const val MENU_VISIBLE = "menu_visible"
fun newIntent(context: Context): Intent { fun newIntent(context: Context, manga: Manga, chapter: Chapter): Intent {
SharedData.put(ReaderEvent(manga, chapter))
return Intent(context, ReaderActivity::class.java) return Intent(context, ReaderActivity::class.java)
} }
} }
@ -81,6 +84,8 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
private var prevChapterBtn: MenuItem? = null private var prevChapterBtn: MenuItem? = null
private val volumeKeysEnabled by lazy { preferences.readWithVolumeKeys().getOrDefault() }
val preferences: PreferencesHelper val preferences: PreferencesHelper
get() = presenter.prefs get() = presenter.prefs
@ -88,6 +93,11 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
super.onCreate(savedState) super.onCreate(savedState)
setContentView(R.layout.activity_reader) setContentView(R.layout.activity_reader)
if (savedState == null && SharedData.get(ReaderEvent::class.java) == null) {
finish()
return
}
setupToolbar(toolbar) setupToolbar(toolbar)
subscriptions = CompositeSubscription() subscriptions = CompositeSubscription()
@ -174,20 +184,34 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
override fun dispatchKeyEvent(event: KeyEvent): Boolean { override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (!isFinishing) { if (!isFinishing) {
val action = event.action when (event.keyCode) {
val keyCode = event.keyCode KeyEvent.KEYCODE_VOLUME_DOWN -> {
when (keyCode) { if (volumeKeysEnabled) {
KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT -> { if (event.action == KeyEvent.ACTION_UP) {
if (action == KeyEvent.ACTION_UP)
viewer?.moveToNext() viewer?.moveToNext()
}
return true return true
} }
KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_DPAD_LEFT -> { }
if (action == KeyEvent.ACTION_UP) KeyEvent.KEYCODE_VOLUME_UP -> {
if (volumeKeysEnabled) {
if (event.action == KeyEvent.ACTION_UP) {
viewer?.moveToPrevious() viewer?.moveToPrevious()
}
return true return true
} }
} }
KeyEvent.KEYCODE_DPAD_RIGHT -> {
if (event.action == KeyEvent.ACTION_UP) {
viewer?.moveToNext()
}
}
KeyEvent.KEYCODE_DPAD_LEFT -> {
if (event.action == KeyEvent.ACTION_UP) {
viewer?.moveToPrevious()
}
}
}
} }
return super.dispatchKeyEvent(event) return super.dispatchKeyEvent(event)
} }
@ -258,7 +282,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
private fun getOrCreateViewer(manga: Manga): BaseReader { private fun getOrCreateViewer(manga: Manga): BaseReader {
val mangaViewer = if (manga.viewer == 0) preferences.defaultViewer else manga.viewer val mangaViewer = if (manga.viewer == 0) preferences.defaultViewer() else manga.viewer
// Try to reuse the viewer using its tag // Try to reuse the viewer using its tag
var fragment: BaseReader? = supportFragmentManager.findFragmentByTag(manga.viewer.toString()) as? BaseReader var fragment: BaseReader? = supportFragmentManager.findFragmentByTag(manga.viewer.toString()) as? BaseReader
@ -479,13 +503,15 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
private fun setCustomBrightness(enabled: Boolean) { private fun setCustomBrightness(enabled: Boolean) {
if (enabled) { if (enabled) {
customBrightnessSubscription = preferences.customBrightnessValue().asObservable() customBrightnessSubscription = preferences.customBrightnessValue().asObservable()
.map { Math.max(0.01f, it) }
.subscribe { setCustomBrightnessValue(it) } .subscribe { setCustomBrightnessValue(it) }
subscriptions.add(customBrightnessSubscription) subscriptions.add(customBrightnessSubscription)
} else { } else {
if (customBrightnessSubscription != null) if (customBrightnessSubscription != null) {
subscriptions.remove(customBrightnessSubscription) subscriptions.remove(customBrightnessSubscription)
setCustomBrightnessValue(-1f) }
setCustomBrightnessValue(WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE)
} }
} }

View File

@ -71,7 +71,7 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
super.onCreate(savedState) super.onCreate(savedState)
if (savedState == null) { if (savedState == null) {
val event = SharedData.remove(ReaderEvent::class.java) ?: return val event = SharedData.get(ReaderEvent::class.java) ?: return
manga = event.manga manga = event.manga
chapter = event.chapter chapter = event.chapter
} else { } else {

View File

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.reader.viewer.base
import com.davemorrissey.labs.subscaleview.decoder.* import com.davemorrissey.labs.subscaleview.decoder.*
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.source.model.Page import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment
import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.ReaderActivity
@ -54,6 +55,11 @@ abstract class BaseReader : BaseFragment() {
lateinit var bitmapDecoderClass: Class<out ImageDecoder> lateinit var bitmapDecoderClass: Class<out ImageDecoder>
private set private set
/**
* Whether tap navigation is enabled or not.
*/
val tappingEnabled by lazy { readerActivity.preferences.readWithTapping().getOrDefault() }
/** /**
* Whether the reader has requested to append a chapter. Used with seamless mode to avoid * Whether the reader has requested to append a chapter. Used with seamless mode to avoid
* restarting requests when changing pages. * restarting requests when changing pages.
@ -72,7 +78,7 @@ abstract class BaseReader : BaseFragment() {
* Returns the active page. * Returns the active page.
*/ */
fun getActivePage(): Page { fun getActivePage(): Page {
return pages[currentPage] return pages.getOrElse(currentPage) { pages[0] }
} }
/** /**

View File

@ -164,9 +164,9 @@ abstract class PagerReader : BaseReader() {
val positionX = e.x val positionX = e.x
if (positionX < pager.width * LEFT_REGION) { if (positionX < pager.width * LEFT_REGION) {
onLeftSideTap() if (tappingEnabled) onLeftSideTap()
} else if (positionX > pager.width * RIGHT_REGION) { } else if (positionX > pager.width * RIGHT_REGION) {
onRightSideTap() if (tappingEnabled) onRightSideTap()
} else { } else {
readerActivity.onCenterSingleTap() readerActivity.onCenterSingleTap()
} }

View File

@ -860,7 +860,7 @@ public class VerticalViewPagerImpl extends ViewGroup {
final float distance = halfHeight + halfHeight * final float distance = halfHeight + halfHeight *
distanceInfluenceForSnapDuration(distanceRatio); distanceInfluenceForSnapDuration(distanceRatio);
int duration = 0; int duration;
velocity = Math.abs(velocity); velocity = Math.abs(velocity);
if (velocity > 0) { if (velocity > 0) {
duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
@ -1017,7 +1017,7 @@ public class VerticalViewPagerImpl extends ViewGroup {
} }
// Locate the currently focused item or add it if needed. // Locate the currently focused item or add it if needed.
int curIndex = -1; int curIndex;
ItemInfo curItem = null; ItemInfo curItem = null;
for (curIndex = 0; curIndex < mItems.size(); curIndex++) { for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
final ItemInfo ii = mItems.get(curIndex); final ItemInfo ii = mItems.get(curIndex);
@ -1177,7 +1177,7 @@ public class VerticalViewPagerImpl extends ViewGroup {
// Base offsets off of oldCurInfo. // Base offsets off of oldCurInfo.
if (oldCurPosition < curItem.position) { if (oldCurPosition < curItem.position) {
int itemIndex = 0; int itemIndex = 0;
ItemInfo ii = null; ItemInfo ii;
float offset = oldCurInfo.offset + oldCurInfo.heightFactor + marginOffset; float offset = oldCurInfo.offset + oldCurInfo.heightFactor + marginOffset;
for (int pos = oldCurPosition + 1; for (int pos = oldCurPosition + 1;
pos <= curItem.position && itemIndex < mItems.size(); pos++) { pos <= curItem.position && itemIndex < mItems.size(); pos++) {
@ -1197,7 +1197,7 @@ public class VerticalViewPagerImpl extends ViewGroup {
} }
} else if (oldCurPosition > curItem.position) { } else if (oldCurPosition > curItem.position) {
int itemIndex = mItems.size() - 1; int itemIndex = mItems.size() - 1;
ItemInfo ii = null; ItemInfo ii;
float offset = oldCurInfo.offset; float offset = oldCurInfo.offset;
for (int pos = oldCurPosition - 1; for (int pos = oldCurPosition - 1;
pos >= curItem.position && itemIndex >= 0; pos--) { pos >= curItem.position && itemIndex >= 0; pos--) {
@ -1561,8 +1561,8 @@ public class VerticalViewPagerImpl extends ViewGroup {
final View child = getChildAt(i); final View child = getChildAt(i);
if (child.getVisibility() != GONE) { if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams(); final LayoutParams lp = (LayoutParams) child.getLayoutParams();
int childLeft = 0; int childLeft;
int childTop = 0; int childTop;
if (lp.isDecor) { if (lp.isDecor) {
final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
@ -1731,7 +1731,7 @@ public class VerticalViewPagerImpl extends ViewGroup {
if (!lp.isDecor) continue; if (!lp.isDecor) continue;
final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
int childTop = 0; int childTop;
switch (vgrav) { switch (vgrav) {
default: default:
childTop = paddingTop; childTop = paddingTop;

View File

@ -126,9 +126,9 @@ class WebtoonReader : BaseReader() {
val positionX = e.x val positionX = e.x
if (positionX < recycler.width * LEFT_REGION) { if (positionX < recycler.width * LEFT_REGION) {
moveToPrevious() if (tappingEnabled) moveToPrevious()
} else if (positionX > recycler.width * RIGHT_REGION) { } else if (positionX > recycler.width * RIGHT_REGION) {
moveToNext() if (tappingEnabled) moveToNext()
} else { } else {
readerActivity.onCenterSingleTap() readerActivity.onCenterSingleTap()
} }

View File

@ -103,21 +103,17 @@ class RecentChaptersFragment : BaseRxFragment<RecentChaptersPresenter>(), Flexib
/** /**
* Open chapter in reader * Open chapter in reader
*
* @param chapter selected chapter * @param chapter selected chapter
*/ */
private fun openChapter(chapter: MangaChapter) { private fun openChapter(chapter: MangaChapter) {
// Start reader event val intent = ReaderActivity.newIntent(activity, chapter.manga, chapter.chapter)
presenter.onOpenChapter(chapter)
//Start reader intent
val intent = ReaderActivity.newIntent(activity)
startActivity(intent) startActivity(intent)
} }
/** /**
* Populate adapter with chapters * Populate adapter with chapters
*
* @param chapters list of chapters * @param chapters list of chapters
*/ */
fun onNextMangaChapters(chapters: List<Any>) { fun onNextMangaChapters(chapters: List<Any>) {

View File

@ -10,9 +10,7 @@ import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.source.SourceManager import eu.kanade.tachiyomi.data.source.SourceManager
import eu.kanade.tachiyomi.event.DownloadChaptersEvent import eu.kanade.tachiyomi.event.DownloadChaptersEvent
import eu.kanade.tachiyomi.event.ReaderEvent
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.SharedData
import rx.Observable import rx.Observable
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
@ -251,14 +249,6 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
return cal.time return cal.time
} }
/**
* Open chapter in reader
* @param item chapter that is opened
*/
fun onOpenChapter(item: MangaChapter) {
SharedData.put(ReaderEvent(item.manga, item.chapter))
}
/** /**
* Download selected chapter * Download selected chapter
* @param selectedChapter chapter that is selected * @param selectedChapter chapter that is selected

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.setting package eu.kanade.tachiyomi.ui.setting
import android.os.Bundle import android.os.Bundle
import android.support.v7.preference.SwitchPreferenceCompat
import android.view.View import android.view.View
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
@ -27,6 +28,10 @@ class SettingsAboutFragment : SettingsNestedFragment() {
*/ */
private var releaseSubscription: Subscription? = null private var releaseSubscription: Subscription? = null
val automaticUpdateToggle by lazy {
findPreference(getString(R.string.pref_enable_automatic_updates_key)) as SwitchPreferenceCompat
}
companion object { companion object {
fun newInstance(resourcePreference: Int, resourceTitle: Int): SettingsNestedFragment { fun newInstance(resourcePreference: Int, resourceTitle: Int): SettingsNestedFragment {
@ -45,13 +50,21 @@ class SettingsAboutFragment : SettingsNestedFragment() {
else else
BuildConfig.VERSION_NAME BuildConfig.VERSION_NAME
if (!BuildConfig.DEBUG && BuildConfig.INCLUDE_UPDATER) {
//Set onClickListener to check for new version //Set onClickListener to check for new version
version.setOnPreferenceClickListener { version.setOnPreferenceClickListener {
if (!BuildConfig.DEBUG && BuildConfig.INCLUDE_UPDATER)
checkVersion()
true true
} }
//TODO One glorious day enable this and add the magnificent option for auto update checking.
// automaticUpdateToggle.isEnabled = true
// automaticUpdateToggle.setOnPreferenceChangeListener { preference, any ->
// val status = any as Boolean
// UpdateDownloaderAlarm.startAlarm(activity, 12, status)
// true
// }
}
buildTime.summary = getFormattedBuildTime() buildTime.summary = getFormattedBuildTime()
} }

View File

@ -14,12 +14,13 @@ import com.nononsenseapps.filepicker.FilePickerActivity
import com.nononsenseapps.filepicker.FilePickerFragment import com.nononsenseapps.filepicker.FilePickerFragment
import com.nononsenseapps.filepicker.LogicHandler import com.nononsenseapps.filepicker.LogicHandler
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.util.inflate import eu.kanade.tachiyomi.util.inflate
import rx.Subscription
import java.io.File import java.io.File
class SettingsDownloadsFragment : SettingsNestedFragment() { class SettingsDownloadsFragment : SettingsNestedFragment() {
val downloadDirPref by lazy { findPreference(getString(R.string.pref_download_directory_key)) }
companion object { companion object {
val DOWNLOAD_DIR_CODE = 103 val DOWNLOAD_DIR_CODE = 103
@ -31,11 +32,16 @@ class SettingsDownloadsFragment : SettingsNestedFragment() {
} }
} }
val downloadDirPref by lazy { findPreference(getString(R.string.pref_download_directory_key)) }
var downloadDirSubscription: Subscription? = null
override fun onViewCreated(view: View, savedState: Bundle?) { override fun onViewCreated(view: View, savedState: Bundle?) {
downloadDirPref.setOnPreferenceClickListener { downloadDirPref.setOnPreferenceClickListener {
val currentDir = preferences.downloadsDirectory().getOrDefault()
val externalDirs = getExternalFilesDirs() val externalDirs = getExternalFilesDirs()
val selectedIndex = externalDirs.indexOf(File(preferences.downloadsDirectory)) val selectedIndex = externalDirs.indexOf(File(currentDir))
MaterialDialog.Builder(activity) MaterialDialog.Builder(activity)
.items(externalDirs + getString(R.string.custom_dir)) .items(externalDirs + getString(R.string.custom_dir))
@ -46,13 +52,12 @@ class SettingsDownloadsFragment : SettingsNestedFragment() {
i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false) i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false)
i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true) i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true)
i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR) i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR)
i.putExtra(FilePickerActivity.EXTRA_START_PATH, preferences.downloadsDirectory) i.putExtra(FilePickerActivity.EXTRA_START_PATH, currentDir)
startActivityForResult(i, DOWNLOAD_DIR_CODE) startActivityForResult(i, DOWNLOAD_DIR_CODE)
} else { } else {
// One of the predefined folders was selected // One of the predefined folders was selected
preferences.downloadsDirectory = text.toString() preferences.downloadsDirectory().set(text.toString())
updateDownloadsDir()
} }
true true
}) })
@ -60,15 +65,14 @@ class SettingsDownloadsFragment : SettingsNestedFragment() {
true true
} }
downloadDirSubscription = preferences.downloadsDirectory().asObservable()
.subscribe { downloadDirPref.summary = it }
} }
override fun onResume() { override fun onDestroyView() {
super.onResume() downloadDirSubscription?.unsubscribe()
updateDownloadsDir() super.onDestroyView()
}
fun updateDownloadsDir() {
downloadDirPref.summary = preferences.downloadsDirectory
} }
fun getExternalFilesDirs(): List<File> { fun getExternalFilesDirs(): List<File> {
@ -81,7 +85,7 @@ class SettingsDownloadsFragment : SettingsNestedFragment() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (data != null && requestCode == DOWNLOAD_DIR_CODE && resultCode == Activity.RESULT_OK) { if (data != null && requestCode == DOWNLOAD_DIR_CODE && resultCode == Activity.RESULT_OK) {
preferences.downloadsDirectory = data.data.path preferences.downloadsDirectory().set(data.data.path)
} }
} }

View File

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.setting
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.support.v14.preference.MultiSelectListPreference
import android.support.v4.app.TaskStackBuilder import android.support.v4.app.TaskStackBuilder
import android.support.v7.preference.Preference import android.support.v7.preference.Preference
import android.view.View import android.view.View
@ -33,13 +34,28 @@ class SettingsGeneralFragment : SettingsNestedFragment() {
findPreference(getString(R.string.pref_library_update_interval_key)) as IntListPreference findPreference(getString(R.string.pref_library_update_interval_key)) as IntListPreference
} }
val updateRestriction by lazy {
findPreference(getString(R.string.pref_library_update_restriction_key)) as MultiSelectListPreference
}
val themePreference by lazy { val themePreference by lazy {
findPreference(getString(R.string.pref_theme_key)) as IntListPreference findPreference(getString(R.string.pref_theme_key)) as IntListPreference
} }
var updateIntervalSubscription: Subscription? = null
var columnsSubscription: Subscription? = null var columnsSubscription: Subscription? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
updateIntervalSubscription = preferences.libraryUpdateInterval().asObservable()
.subscribe { updateRestriction.isVisible = it > 0 }
columnsSubscription = Observable.combineLatest(
preferences.portraitColumns().asObservable(),
preferences.landscapeColumns().asObservable())
{ portraitColumns, landscapeColumns -> Pair(portraitColumns, landscapeColumns) }
.subscribe { updateColumnsSummary(it.first, it.second) }
updateInterval.setOnPreferenceChangeListener { preference, newValue -> updateInterval.setOnPreferenceChangeListener { preference, newValue ->
LibraryUpdateAlarm.startAlarm(activity, (newValue as String).toInt()) LibraryUpdateAlarm.startAlarm(activity, (newValue as String).toInt())
true true
@ -57,17 +73,11 @@ class SettingsGeneralFragment : SettingsNestedFragment() {
} }
} }
override fun onResume() { override fun onDestroyView() {
super.onResume() updateIntervalSubscription?.unsubscribe()
columnsSubscription = Observable.combineLatest(preferences.portraitColumns().asObservable(),
preferences.landscapeColumns().asObservable(),
{ portraitColumns, landscapeColumns -> Pair(portraitColumns, landscapeColumns) })
.subscribe { updateColumnsSummary(it.first, it.second) }
}
override fun onPause() {
columnsSubscription?.unsubscribe() columnsSubscription?.unsubscribe()
super.onPause() super.onDestroyView()
} }
override fun onDisplayPreferenceDialog(preference: Preference) { override fun onDisplayPreferenceDialog(preference: Preference) {

View File

@ -44,9 +44,5 @@ open class SettingsNestedFragment : PreferenceFragment() {
get() = settingsActivity.preferences get() = settingsActivity.preferences
val fragmentManagerCompat: FragmentManager val fragmentManagerCompat: FragmentManager
get() = if (Build.VERSION.SDK_INT >= 17) { get() = if (Build.VERSION.SDK_INT >= 17) childFragmentManager else fragmentManager
childFragmentManager
} else {
fragmentManager
}
} }

View File

@ -6,7 +6,6 @@ import android.support.v14.preference.MultiSelectListPreference
import android.support.v7.preference.Preference import android.support.v7.preference.Preference
import android.support.v7.preference.PreferenceGroup import android.support.v7.preference.PreferenceGroup
import android.view.View import android.view.View
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.source.base.Source import eu.kanade.tachiyomi.data.source.base.Source
import eu.kanade.tachiyomi.data.source.getLanguages import eu.kanade.tachiyomi.data.source.getLanguages
@ -65,7 +64,7 @@ class SettingsSourcesFragment : SettingsNestedFragment() {
fun createSource(source: Source): Preference { fun createSource(source: Source): Preference {
return LoginPreference(preferenceManager.context).apply { return LoginPreference(preferenceManager.context).apply {
key = PreferencesHelper.SOURCE_ACCOUNT_USERNAME + source.id key = preferences.keys.sourceUsername(source.id)
title = source.visibleName title = source.visibleName
setOnPreferenceClickListener { setOnPreferenceClickListener {
@ -80,7 +79,7 @@ class SettingsSourcesFragment : SettingsNestedFragment() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == SOURCE_CHANGE_REQUEST) { if (requestCode == SOURCE_CHANGE_REQUEST) {
val pref = findPreference(PreferencesHelper.SOURCE_ACCOUNT_USERNAME + resultCode) as? LoginPreference val pref = findPreference(preferences.keys.sourceUsername(resultCode)) as? LoginPreference
pref?.notifyChanged() pref?.notifyChanged()
} }
} }

View File

@ -4,7 +4,6 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.support.v7.preference.PreferenceCategory import android.support.v7.preference.PreferenceCategory
import android.view.View import android.view.View
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.widget.preference.LoginPreference import eu.kanade.tachiyomi.widget.preference.LoginPreference
import eu.kanade.tachiyomi.widget.preference.MangaSyncLoginDialog import eu.kanade.tachiyomi.widget.preference.MangaSyncLoginDialog
@ -27,7 +26,7 @@ class SettingsSyncFragment : SettingsNestedFragment() {
for (sync in settingsActivity.syncManager.services) { for (sync in settingsActivity.syncManager.services) {
val pref = LoginPreference(themedContext).apply { val pref = LoginPreference(themedContext).apply {
key = PreferencesHelper.MANGASYNC_ACCOUNT_USERNAME + sync.id key = preferences.keys.syncUsername(sync.id)
title = sync.name title = sync.name
setOnPreferenceClickListener { setOnPreferenceClickListener {
@ -44,7 +43,7 @@ class SettingsSyncFragment : SettingsNestedFragment() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == SYNC_CHANGE_REQUEST) { if (requestCode == SYNC_CHANGE_REQUEST) {
val pref = findPreference(PreferencesHelper.MANGASYNC_ACCOUNT_USERNAME + resultCode) as? LoginPreference val pref = findPreference(preferences.keys.syncUsername(resultCode)) as? LoginPreference
pref?.notifyChanged() pref?.notifyChanged()
} }
} }

View File

@ -11,7 +11,7 @@ object DeviceUtil {
val intent = context.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED)) val intent = context.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
intent?.let { intent?.let {
val plugged = it.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) val plugged = it.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1)
return plugged == BatteryManager.BATTERY_PLUGGED_AC || plugged == BatteryManager.BATTERY_PLUGGED_USB return plugged == BatteryManager.BATTERY_PLUGGED_AC || plugged == BatteryManager.BATTERY_PLUGGED_USB || plugged == BatteryManager.BATTERY_PLUGGED_WIRELESS
} }
return false return false
} }

View File

@ -5,10 +5,6 @@ import java.io.IOException;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import okio.BufferedSink;
import okio.BufferedSource;
import okio.Okio;
public final class DiskUtils { public final class DiskUtils {
private DiskUtils() { private DiskUtils() {
@ -39,34 +35,6 @@ public final class DiskUtils {
return sb.toString(); return sb.toString();
} }
public static File saveBufferedSourceToDirectory(BufferedSource bufferedSource, File directory, String name) throws IOException {
createDirectory(directory);
File writeFile = new File(directory, name);
if (writeFile.exists()) {
if (writeFile.delete()) {
writeFile = new File(directory, name);
} else {
throw new IOException("Failed Deleting Existing File for Overwrite");
}
}
BufferedSink bufferedSink = null;
try {
bufferedSink = Okio.buffer(Okio.sink(writeFile));
bufferedSink.writeAll(bufferedSource);
} catch (Exception e) {
writeFile.delete();
throw new IOException("Unable to save image");
} finally {
if (bufferedSink != null) {
bufferedSink.close();
}
}
return writeFile;
}
public static void deleteFiles(File inputFile) { public static void deleteFiles(File inputFile) {
if (inputFile.isDirectory()) { if (inputFile.isDirectory()) {
for (File childFile : inputFile.listFiles()) { for (File childFile : inputFile.listFiles()) {
@ -74,6 +42,7 @@ public final class DiskUtils {
} }
} }
//noinspection ResultOfMethodCallIgnored
inputFile.delete(); inputFile.delete();
} }

View File

@ -0,0 +1,62 @@
package eu.kanade.tachiyomi.util
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import okio.BufferedSource
import okio.Okio
import java.io.File
import java.io.OutputStream
/**
* Saves the given source to a file and closes it. Directories will be created if needed.
*
* @param file the file where the source is copied.
*/
fun BufferedSource.saveTo(file: File) {
try {
// Create parent dirs if needed
file.parentFile.mkdirs()
// Copy to destination
saveTo(file.outputStream())
} catch (e: Exception) {
close()
file.delete()
throw e
}
}
/**
* Saves the given source to an output stream and closes both resources.
*
* @param stream the stream where the source is copied.
*/
fun BufferedSource.saveTo(stream: OutputStream) {
use { input ->
Okio.buffer(Okio.sink(stream)).use {
it.writeAll(input)
it.flush()
}
}
}
/**
* Saves the given source to an output stream and closes both resources.
* The source is expected to be an image, and it may reencode the image.
*
* @param stream the stream where the source is copied.
* @param reencode whether to reencode the image or not.
*/
fun BufferedSource.saveImageTo(stream: OutputStream, reencode: Boolean = false) {
if (reencode) {
use {
val bitmap = BitmapFactory.decodeStream(it.inputStream())
stream.use {
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it)
}
bitmap.recycle()
}
} else {
saveTo(stream)
}
}

View File

@ -14,7 +14,7 @@ object SharedData {
/** /**
* Map where the objects are saved. * Map where the objects are saved.
*/ */
private val map = HashMap<Class<*>, Any>() val map = HashMap<Class<*>, Any>()
/** /**
* Publish an object to the shared data. * Publish an object to the shared data.
@ -42,4 +42,14 @@ object SharedData {
*/ */
fun <T : Any> remove(classType: Class<T>) = get(classType)?.apply { map.remove(classType) } fun <T : Any> remove(classType: Class<T>) = get(classType)?.apply { map.remove(classType) }
/**
* Returns an object from the shared data or introduces a new one with the given function.
*
* @param classType the class of the object to retrieve.
* @param fn the function to execute if it didn't find the object.
* @return an object of type T.
*/
@Suppress("UNCHECKED_CAST")
inline fun <T : Any> getOrPut(classType: Class<T>, fn: () -> T) = map.getOrPut(classType, fn) as T
} }

View File

@ -6,6 +6,7 @@ import android.graphics.Typeface
import android.util.AttributeSet import android.util.AttributeSet
import android.widget.TextView import android.widget.TextView
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import java.util.*
class PTSansTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : class PTSansTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
@ -14,6 +15,9 @@ class PTSansTextView @JvmOverloads constructor(context: Context, attrs: Attribut
companion object { companion object {
const val PTSANS_NARROW = 0 const val PTSANS_NARROW = 0
const val PTSANS_NARROW_BOLD = 1 const val PTSANS_NARROW_BOLD = 1
// Map where typefaces are cached
private val typefaces = HashMap<Int, Typeface>(2)
} }
init { init {
@ -22,11 +26,13 @@ class PTSansTextView @JvmOverloads constructor(context: Context, attrs: Attribut
val typeface = values.getInt(R.styleable.PTSansTextView_typeface, 0) val typeface = values.getInt(R.styleable.PTSansTextView_typeface, 0)
when (typeface) { setTypeface(typefaces.getOrPut(typeface) {
PTSANS_NARROW -> setTypeface(Typeface.createFromAsset(context.assets, "fonts/PTSans-Narrow.ttf")) Typeface.createFromAsset(context.assets, when (typeface) {
PTSANS_NARROW_BOLD -> setTypeface(Typeface.createFromAsset(context.assets, "fonts/PTSans-NarrowBold.ttf")) PTSANS_NARROW -> "fonts/PTSans-Narrow.ttf"
PTSANS_NARROW_BOLD -> "fonts/PTSans-NarrowBold.ttf"
else -> throw IllegalArgumentException("Font not found " + typeface) else -> throw IllegalArgumentException("Font not found " + typeface)
} })
})
values.recycle() values.recycle()
} }

View File

@ -11,8 +11,8 @@ import com.afollestad.materialdialogs.MaterialDialog
import com.dd.processbutton.iml.ActionProcessButton import com.dd.processbutton.iml.ActionProcessButton
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.setting.SettingsActivity
import eu.kanade.tachiyomi.ui.base.listener.SimpleTextWatcher import eu.kanade.tachiyomi.ui.base.listener.SimpleTextWatcher
import eu.kanade.tachiyomi.ui.setting.SettingsActivity
import kotlinx.android.synthetic.main.pref_account_login.view.* import kotlinx.android.synthetic.main.pref_account_login.view.*
import rx.Subscription import rx.Subscription
@ -30,7 +30,7 @@ abstract class LoginDialogPreference : DialogFragment() {
val dialog = MaterialDialog.Builder(activity) val dialog = MaterialDialog.Builder(activity)
.customView(R.layout.pref_account_login, false) .customView(R.layout.pref_account_login, false)
.negativeText(android.R.string.cancel) .negativeText(android.R.string.cancel)
.build(); .build()
onViewCreated(dialog.view, savedState) onViewCreated(dialog.view, savedState)

View File

@ -34,8 +34,8 @@ class MangaSyncLoginDialog : LoginDialogPreference() {
override fun setCredentialsOnView(view: View) = with(view) { override fun setCredentialsOnView(view: View) = with(view) {
dialog_title.text = getString(R.string.login_title, sync.name) dialog_title.text = getString(R.string.login_title, sync.name)
username.setText(preferences.getMangaSyncUsername(sync)) username.setText(preferences.mangaSyncUsername(sync))
password.setText(preferences.getMangaSyncPassword(sync)) password.setText(preferences.mangaSyncPassword(sync))
} }
override fun checkLogin() { override fun checkLogin() {

View File

@ -34,8 +34,8 @@ class SourceLoginDialog : LoginDialogPreference() {
override fun setCredentialsOnView(view: View) = with(view) { override fun setCredentialsOnView(view: View) = with(view) {
dialog_title.text = getString(R.string.login_title, source.visibleName) dialog_title.text = getString(R.string.login_title, source.visibleName)
username.setText(preferences.getSourceUsername(source)) username.setText(preferences.sourceUsername(source))
password.setText(preferences.getSourcePassword(source)) password.setText(preferences.sourcePassword(source))
} }
override fun checkLogin() { override fun checkLogin() {

Binary file not shown.

After

Width:  |  Height:  |  Size: 717 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 871 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -27,7 +27,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentRight="true" android:layout_alignParentRight="true"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:text="Edit" android:text="@string/action_edit"
style="@style/TextAppearance.Medium.Button"/> style="@style/TextAppearance.Medium.Button"/>
</RelativeLayout> </RelativeLayout>

View File

@ -6,27 +6,23 @@
<item <item
android:id="@+id/action_download" android:id="@+id/action_download"
android:title="@string/action_download" android:title="@string/action_download"
android:icon="@drawable/ic_file_download_white_24dp"
android:visible="true" android:visible="true"
app:showAsAction="ifRoom"/> app:showAsAction="ifRoom"/>
<item <item
android:id="@+id/action_delete" android:id="@+id/action_delete"
android:title="@string/action_delete" android:title="@string/action_delete"
android:icon="@drawable/ic_delete_white_24dp"
android:visible="false" android:visible="false"
app:showAsAction="ifRoom"/> app:showAsAction="ifRoom"/>
<item <item
android:id="@+id/action_mark_as_read" android:id="@+id/action_mark_as_read"
android:title="@string/action_mark_as_read" android:title="@string/action_mark_as_read"
android:icon="@drawable/ic_done_all_white_24dp"
app:showAsAction="ifRoom"/> app:showAsAction="ifRoom"/>
<item <item
android:id="@+id/action_mark_as_unread" android:id="@+id/action_mark_as_unread"
android:title="@string/action_mark_as_unread" android:title="@string/action_mark_as_unread"
android:icon="@drawable/ic_done_all_grey_24dp"
app:showAsAction="ifRoom"/> app:showAsAction="ifRoom"/>
</menu> </menu>

View File

@ -126,4 +126,14 @@
<item>48</item> <item>48</item>
</string-array> </string-array>
<string-array name="library_update_restrictions">
<item>@string/wifi</item>
<item>@string/charging</item>
</string-array>
<string-array name="library_update_restrictions_values">
<item>wifi</item>
<item>ac</item>
</string-array>
</resources> </resources>

View File

@ -13,10 +13,10 @@
<string name="pref_library_columns_landscape_key">pref_library_columns_landscape_key</string> <string name="pref_library_columns_landscape_key">pref_library_columns_landscape_key</string>
<string name="pref_library_update_interval_key">pref_library_update_interval_key</string> <string name="pref_library_update_interval_key">pref_library_update_interval_key</string>
<string name="pref_update_only_non_completed_key">pref_update_only_non_completed_key</string> <string name="pref_update_only_non_completed_key">pref_update_only_non_completed_key</string>
<string name="pref_update_only_when_charging_key">pref_update_only_when_charging_key</string>
<string name="pref_auto_update_manga_sync_key">pref_auto_update_manga_sync_key</string> <string name="pref_auto_update_manga_sync_key">pref_auto_update_manga_sync_key</string>
<string name="pref_ask_update_manga_sync_key">pref_ask_update_manga_sync_key</string> <string name="pref_ask_update_manga_sync_key">pref_ask_update_manga_sync_key</string>
<string name="pref_theme_key">pref_theme_key</string> <string name="pref_theme_key">pref_theme_key</string>
<string name="pref_library_update_restriction_key">library_update_restriction</string>
<string name="pref_default_viewer_key">pref_default_viewer_key</string> <string name="pref_default_viewer_key">pref_default_viewer_key</string>
<string name="pref_image_scale_type_key">pref_image_scale_type_key</string> <string name="pref_image_scale_type_key">pref_image_scale_type_key</string>
@ -31,6 +31,9 @@
<string name="pref_reader_theme_key">pref_reader_theme_key</string> <string name="pref_reader_theme_key">pref_reader_theme_key</string>
<string name="pref_image_decoder_key">pref_image_decoder_key</string> <string name="pref_image_decoder_key">pref_image_decoder_key</string>
<string name="pref_seamless_mode_key">pref_seamless_mode_key</string> <string name="pref_seamless_mode_key">pref_seamless_mode_key</string>
<string name="pref_read_with_volume_keys_key">reader_volume_keys</string>
<string name="pref_read_with_tapping_key">reader_tap</string>
<string name="pref_reencode_key">reencode_image</string>
<string name="pref_filter_downloaded_key">pref_filter_downloaded_key</string> <string name="pref_filter_downloaded_key">pref_filter_downloaded_key</string>
<string name="pref_filter_unread_key">pref_filter_unread_key</string> <string name="pref_filter_unread_key">pref_filter_unread_key</string>
@ -49,8 +52,10 @@
<string name="pref_clear_chapter_cache_key">pref_clear_chapter_cache_key</string> <string name="pref_clear_chapter_cache_key">pref_clear_chapter_cache_key</string>
<string name="pref_clear_database_key">pref_clear_database_key</string> <string name="pref_clear_database_key">pref_clear_database_key</string>
<string name="pref_version">pref_version</string> <string name="pref_version">pref_version</string>
<string name="pref_build_time">pref_build_time</string> <string name="pref_build_time">pref_build_time</string>
<string name="pref_enable_automatic_updates_key">pref_enable_automatic_updates_key</string>
<string name="pref_display_catalogue_as_list">pref_display_catalogue_as_list</string> <string name="pref_display_catalogue_as_list">pref_display_catalogue_as_list</string>
<string name="pref_last_catalogue_source_key">pref_last_catalogue_source_key</string> <string name="pref_last_catalogue_source_key">pref_last_catalogue_source_key</string>

View File

@ -51,6 +51,7 @@
<string name="action_cancel">Cancel</string> <string name="action_cancel">Cancel</string>
<string name="action_sort">Sort</string> <string name="action_sort">Sort</string>
<string name="action_force">Force refresh</string> <string name="action_force">Force refresh</string>
<string name="action_install">Install</string>
<!-- Operations --> <!-- Operations -->
<string name="deleting">Deleting…</string> <string name="deleting">Deleting…</string>
@ -72,8 +73,6 @@
<string name="landscape">Landscape</string> <string name="landscape">Landscape</string>
<string name="default_columns">Default</string> <string name="default_columns">Default</string>
<string name="pref_library_update_interval">Library update frequency</string> <string name="pref_library_update_interval">Library update frequency</string>
<string name="pref_update_only_non_completed">Only update incomplete manga</string>
<string name="pref_update_only_when_charging">Only update when charging</string>
<string name="update_never">Manual</string> <string name="update_never">Manual</string>
<string name="update_1hour">Hourly</string> <string name="update_1hour">Hourly</string>
<string name="update_2hour">Every 2 hours</string> <string name="update_2hour">Every 2 hours</string>
@ -82,13 +81,17 @@
<string name="update_12hour">Every 12 hours</string> <string name="update_12hour">Every 12 hours</string>
<string name="update_24hour">Daily</string> <string name="update_24hour">Daily</string>
<string name="update_48hour">Every 2 days</string> <string name="update_48hour">Every 2 days</string>
<string name="pref_library_update_restriction">Library update restrictions</string>
<string name="pref_library_update_restriction_summary">Update only when the conditions are met</string>
<string name="wifi">Wi-Fi</string>
<string name="charging">Charging</string>
<string name="pref_update_only_non_completed">Only update incomplete manga</string>
<string name="pref_auto_update_manga_sync">Sync chapters after reading</string> <string name="pref_auto_update_manga_sync">Sync chapters after reading</string>
<string name="pref_ask_update_manga_sync">Confirm before updating</string> <string name="pref_ask_update_manga_sync">Confirm before updating</string>
<string name="pref_theme">Application theme</string> <string name="pref_theme">Application theme</string>
<string name="light_theme">Main theme</string> <string name="light_theme">Main theme</string>
<string name="dark_theme">Dark theme</string> <string name="dark_theme">Dark theme</string>
<!-- Reader section --> <!-- Reader section -->
<string name="pref_hide_status_bar">Hide status bar</string> <string name="pref_hide_status_bar">Hide status bar</string>
<string name="pref_lock_orientation">Lock orientation</string> <string name="pref_lock_orientation">Lock orientation</string>
@ -97,6 +100,9 @@
<string name="pref_custom_brightness">Use custom brightness</string> <string name="pref_custom_brightness">Use custom brightness</string>
<string name="pref_seamless_mode">Seamless chapter transitions</string> <string name="pref_seamless_mode">Seamless chapter transitions</string>
<string name="pref_keep_screen_on">Keep screen on</string> <string name="pref_keep_screen_on">Keep screen on</string>
<string name="pref_reader_navigation">Navigation</string>
<string name="pref_read_with_volume_keys">Volume keys</string>
<string name="pref_read_with_tapping">Tapping</string>
<string name="pref_reader_theme">Background color</string> <string name="pref_reader_theme">Background color</string>
<string name="white_background">White</string> <string name="white_background">White</string>
<string name="black_background">Black</string> <string name="black_background">Black</string>
@ -155,11 +161,16 @@
<string name="pref_clear_database">Clear database</string> <string name="pref_clear_database">Clear database</string>
<string name="pref_clear_database_summary">Delete manga and chapters that are not in your library</string> <string name="pref_clear_database_summary">Delete manga and chapters that are not in your library</string>
<string name="clear_database_confirmation">Are you sure? Read chapters and progress of non-library manga will be lost</string> <string name="clear_database_confirmation">Are you sure? Read chapters and progress of non-library manga will be lost</string>
<string name="pref_show_warning_message">Show warnings</string>
<string name="pref_show_warning_message_summary">Show warning messages during library sync </string>
<string name="pref_reencode">Reencode images</string>
<string name="pref_reencode_summary">Enable reencoding if images can\'t be decoded. Expect best results with Skia</string>
<!-- About section --> <!-- About section -->
<string name="version">Version</string> <string name="version">Version</string>
<string name="build_time">Build time</string> <string name="build_time">Build time</string>
<string name="pref_enable_automatic_updates">Check for updates</string>
<string name="pref_enable_automatic_updates_summary">Automatically check for application updates</string>
<!-- ACRA --> <!-- ACRA -->
<string name="pref_enable_acra">Send crash reports</string> <string name="pref_enable_acra">Send crash reports</string>
<string name="pref_acra_summary">Helps fix any bugs. No sensitive data will be sent</string> <string name="pref_acra_summary">Helps fix any bugs. No sensitive data will be sent</string>
@ -275,6 +286,13 @@
<string name="update_check_download_started">Download started</string> <string name="update_check_download_started">Download started</string>
<string name="update_check_look_for_updates">Looking for updates</string> <string name="update_check_look_for_updates">Looking for updates</string>
<!--UpdateCheck Notifications-->
<string name="update_check_notification_file_download">Download update</string>
<string name="update_check_notification_download_in_progress">Download in progress</string>
<string name="update_check_notification_download_complete">Download complete</string>
<string name="update_check_notification_download_error">Download error</string>
<string name="update_check_notification_update_available">Update available</string>
<!--Content Description--> <!--Content Description-->
<string name="description_backdrop">Backdrop image of selected manga</string> <string name="description_backdrop">Backdrop image of selected manga</string>
<string name="description_cover">Cover of selected manga</string> <string name="description_cover">Cover of selected manga</string>

View File

@ -3,19 +3,26 @@
xmlns:android="http://schemas.android.com/apk/res/android"> xmlns:android="http://schemas.android.com/apk/res/android">
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:defaultValue="true"
android:key="acra.enable" android:key="acra.enable"
android:title="@string/pref_enable_acra"
android:summary="@string/pref_acra_summary" android:summary="@string/pref_acra_summary"
android:defaultValue="true"/> android:title="@string/pref_enable_acra"/>
<!--<SwitchPreferenceCompat-->
<!--android:defaultValue="false"-->
<!--android:enabled="false"-->
<!--android:key="@string/pref_enable_automatic_updates_key"-->
<!--android:summary="@string/pref_enable_automatic_updates_summary"-->
<!--android:title="@string/pref_enable_automatic_updates"/>-->
<Preference <Preference
android:key="@string/pref_version" android:key="@string/pref_version"
android:title="@string/version" android:persistent="false"
android:persistent="false" /> android:title="@string/version"/>
<Preference <Preference
android:key="@string/pref_build_time" android:key="@string/pref_build_time"
android:title="@string/build_time" android:persistent="false"
android:persistent="false" /> android:title="@string/build_time"/>
</android.support.v7.preference.PreferenceScreen> </android.support.v7.preference.PreferenceScreen>

View File

@ -3,12 +3,18 @@
xmlns:android="http://schemas.android.com/apk/res/android"> xmlns:android="http://schemas.android.com/apk/res/android">
<Preference <Preference
android:title="@string/pref_clear_chapter_cache" android:key="@string/pref_clear_chapter_cache_key"
android:key="@string/pref_clear_chapter_cache_key" /> android:title="@string/pref_clear_chapter_cache"/>
<Preference <Preference
android:title="@string/pref_clear_database"
android:key="@string/pref_clear_database_key" android:key="@string/pref_clear_database_key"
android:summary="@string/pref_clear_database_summary"/> android:summary="@string/pref_clear_database_summary"
android:title="@string/pref_clear_database"/>
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="@string/pref_reencode_key"
android:summary="@string/pref_reencode_summary"
android:title="@string/pref_reencode"/>
</android.support.v7.preference.PreferenceScreen> </android.support.v7.preference.PreferenceScreen>

View File

@ -24,14 +24,16 @@
android:summary="%s" android:summary="%s"
android:title="@string/pref_library_update_interval"/> android:title="@string/pref_library_update_interval"/>
<MultiSelectListPreference
android:entries="@array/library_update_restrictions"
android:entryValues="@array/library_update_restrictions_values"
android:key="@string/pref_library_update_restriction_key"
android:summary="@string/pref_library_update_restriction_summary"
android:title="@string/pref_library_update_restriction" />
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:defaultValue="false" android:defaultValue="false"
android:key="@string/pref_update_only_non_completed_key" android:key="@string/pref_update_only_non_completed_key"
android:title="@string/pref_update_only_non_completed"/> android:title="@string/pref_update_only_non_completed"/>
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="@string/pref_update_only_when_charging_key"
android:title="@string/pref_update_only_when_charging"/>
</android.support.v7.preference.PreferenceScreen> </android.support.v7.preference.PreferenceScreen>

View File

@ -2,30 +2,6 @@
<android.support.v7.preference.PreferenceScreen <android.support.v7.preference.PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"> xmlns:android="http://schemas.android.com/apk/res/android">
<SwitchPreferenceCompat android:title="@string/pref_hide_status_bar"
android:key="@string/pref_hide_status_bar_key"
android:defaultValue="true" />
<SwitchPreferenceCompat android:title="@string/pref_enable_transitions"
android:key="@string/pref_enable_transitions_key"
android:defaultValue="true" />
<SwitchPreferenceCompat android:title="@string/pref_show_page_number"
android:key="@string/pref_show_page_number_key"
android:defaultValue="true" />
<SwitchPreferenceCompat android:title="@string/pref_custom_brightness"
android:key="@string/pref_custom_brightness_key"
android:defaultValue="false" />
<SwitchPreferenceCompat android:title="@string/pref_keep_screen_on"
android:key="@string/pref_keep_screen_on_key"
android:defaultValue="true" />
<SwitchPreferenceCompat android:title="@string/pref_seamless_mode"
android:key="@string/pref_seamless_mode_key"
android:defaultValue="true" />
<eu.kanade.tachiyomi.widget.preference.IntListPreference <eu.kanade.tachiyomi.widget.preference.IntListPreference
android:title="@string/pref_viewer_type" android:title="@string/pref_viewer_type"
android:key="@string/pref_default_viewer_key" android:key="@string/pref_default_viewer_key"
@ -74,4 +50,49 @@
android:defaultValue="0" android:defaultValue="0"
android:summary="%s" /> android:summary="%s" />
<SwitchPreferenceCompat
android:title="@string/pref_hide_status_bar"
android:key="@string/pref_hide_status_bar_key"
android:defaultValue="true" />
<SwitchPreferenceCompat
android:title="@string/pref_enable_transitions"
android:key="@string/pref_enable_transitions_key"
android:defaultValue="true" />
<SwitchPreferenceCompat
android:title="@string/pref_show_page_number"
android:key="@string/pref_show_page_number_key"
android:defaultValue="true" />
<SwitchPreferenceCompat
android:title="@string/pref_custom_brightness"
android:key="@string/pref_custom_brightness_key"
android:defaultValue="false" />
<SwitchPreferenceCompat
android:title="@string/pref_keep_screen_on"
android:key="@string/pref_keep_screen_on_key"
android:defaultValue="true" />
<SwitchPreferenceCompat
android:title="@string/pref_seamless_mode"
android:key="@string/pref_seamless_mode_key"
android:defaultValue="true" />
<PreferenceCategory
android:title="@string/pref_reader_navigation">
<SwitchPreferenceCompat
android:title="@string/pref_read_with_tapping"
android:key="@string/pref_read_with_tapping_key"
android:defaultValue="true" />
<SwitchPreferenceCompat
android:title="@string/pref_read_with_volume_keys"
android:key="@string/pref_read_with_volume_keys_key"
android:defaultValue="false" />
</PreferenceCategory>
</android.support.v7.preference.PreferenceScreen> </android.support.v7.preference.PreferenceScreen>

View File

@ -101,7 +101,7 @@ public class LibraryUpdateAlarmTest {
@Test @Test
public void testLibraryUpdateServiceIsStartedWhenUpdateIntentIsReceived() { public void testLibraryUpdateServiceIsStartedWhenUpdateIntentIsReceived() {
Intent intent = new Intent(context, LibraryUpdateService.class); Intent intent = new Intent(context, LibraryUpdateService.class);
intent.putExtra("is_forced", false); intent.putExtra("is_manual", false);
assertThat(app.getNextStartedService()).isNotEqualTo(intent); assertThat(app.getNextStartedService()).isNotEqualTo(intent);
LibraryUpdateAlarm alarm = new LibraryUpdateAlarm(); LibraryUpdateAlarm alarm = new LibraryUpdateAlarm();