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 |
|-------|----------|-------------|
| [![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)

View File

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

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi"
xmlns:android="http://schemas.android.com/apk/res/android">
<manifest 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.ACCESS_NETWORK_STATE" />
@ -68,7 +68,19 @@
</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
@ -79,6 +91,15 @@
</intent-filter>
</receiver>
<receiver
android:name=".data.updater.UpdateDownloaderAlarm">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="eu.kanade.CHECK_UPDATE"/>
</intent-filter>
</receiver>
<meta-data
android:name="eu.kanade.tachiyomi.data.cache.CoverGlideModule"
android:value="GlideModule" />

View File

@ -7,6 +7,7 @@ import com.google.gson.reflect.TypeToken
import com.jakewharton.disklrucache.DiskLruCache
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.util.DiskUtils
import eu.kanade.tachiyomi.util.saveImageTo
import okhttp3.Response
import okio.Okio
import rx.Observable
@ -184,7 +185,7 @@ class ChapterCache(private val context: Context) {
* @throws IOException image error.
*/
@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).
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")
// Get OutputStream and write image with Okio.
Okio.buffer(Okio.sink(editor.newOutputStream(0))).use {
it.writeAll(response.body().source())
it.flush()
}
response.body().source().saveImageTo(editor.newOutputStream(0), reencode)
diskCache.flush()
editor.commit()
} catch (e: Exception) {
response.body().close()
throw IOException("Unable to save image")
} finally {
response.body().close()
editor?.abortUnlessCommitted()
}
}

View File

@ -33,7 +33,7 @@ class CoverCache(private val context: Context) {
* @param headers headers included in Glide request.
* @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.
if (thumbnailUrl.isNullOrEmpty())
return
@ -62,7 +62,7 @@ class CoverCache(private val context: Context) {
* @param headers headers included in Glide request.
* @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.
if (thumbnailUrl.isNullOrEmpty())
return

View File

@ -260,6 +260,14 @@ open class DatabaseHelper(context: Context) {
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
fun getCategories() = db.get()

View File

@ -95,6 +95,13 @@ public class Manga implements Serializable {
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) {
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.DownloadQueue
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.base.Source
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.event.DownloadChaptersEvent
import eu.kanade.tachiyomi.util.DiskUtils
import eu.kanade.tachiyomi.util.DynamicConcurrentMergeOperator
import eu.kanade.tachiyomi.util.UrlUtil
import eu.kanade.tachiyomi.util.toast
import eu.kanade.tachiyomi.util.*
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
@ -26,10 +24,9 @@ import rx.subjects.BehaviorSubject
import rx.subjects.PublishSubject
import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.io.FileReader
import java.io.IOException
import java.util.*
import java.util.concurrent.TimeUnit
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
Observable.just(download.pages)
return Observable.defer<Download> { pageListObservable
return Observable.defer { pageListObservable
.doOnNext { pages ->
download.downloadedImages = 0
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> {
page.status = Page.DOWNLOAD_IMAGE
return source.getImageProgressResponse(page)
.flatMap({ resp ->
DiskUtils.saveBufferedSourceToDirectory(resp.body().source(), directory, filename)
.flatMap {
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)
}).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
@ -306,26 +314,14 @@ class DownloadManager(private val context: Context, private val sourceManager: S
val chapterDir = getAbsoluteChapterDirectory(source, manga, chapter)
val pagesFile = File(chapterDir, PAGE_LIST_FILE)
var reader: JsonReader? = null
try {
if (pagesFile.exists()) {
reader = JsonReader(FileReader(pagesFile.absolutePath))
val collectionType = object : TypeToken<List<Page>>() {
}.type
return gson.fromJson<List<Page>>(reader, collectionType)
return try {
JsonReader(FileReader(pagesFile)).use {
val collectionType = object : TypeToken<List<Page>>() {}.type
gson.fromJson(it, collectionType)
}
} catch (e: Exception) {
Timber.e(e.cause, e.message)
} finally {
if (reader != null) try {
reader.close()
} catch (e: IOException) {
/* Do nothing */
}
null
}
return null
}
// 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 pagesFile = File(chapterDir, PAGE_LIST_FILE)
var out: FileOutputStream? = null
try {
out = FileOutputStream(pagesFile)
out.write(gson.toJson(pages).toByteArray())
out.flush()
} catch (e: IOException) {
Timber.e(e.cause, e.message)
} finally {
if (out != null) try {
out.close()
} catch (e: IOException) {
/* Do nothing */
pagesFile.outputStream().use {
try {
it.write(gson.toJson(pages).toByteArray())
it.flush()
} catch (e: Exception) {
Timber.e(e, e.message)
}
}
}
@ -365,7 +354,7 @@ class DownloadManager(private val context: Context, private val sourceManager: S
File.separator +
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

View File

@ -4,9 +4,9 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.source.model.Page
import rx.Observable
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>()

View File

@ -9,6 +9,8 @@ import android.os.IBinder
import android.os.PowerManager
import android.support.v4.app.NotificationCompat
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.R
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.source.SourceManager
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.AndroidComponentUtil
import eu.kanade.tachiyomi.util.DeviceUtil
import eu.kanade.tachiyomi.util.notification
import eu.kanade.tachiyomi.util.notificationManager
import eu.kanade.tachiyomi.util.*
import rx.Observable
import rx.Subscription
import rx.schedulers.Schedulers
import timber.log.Timber
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
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
* started calling the [start] method. If it's already running, it won't do anything.
@ -77,6 +51,30 @@ class LibraryUpdateService : Service() {
companion object {
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
* 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
* is irrelevant, because everything required is fetched in [updateLibrary].
* @param intent the intent from [start].
* Method called when the service receives an intent.
* @param intent the start intent from.
* @param flags the flags of the command.
* @param startId the start id of this command.
* @return the start value of the command.
*/
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// If there's no network available, set a component to start this service again when
// a connection is available.
if (!DeviceUtil.isNetworkConnected(this)) {
Timber.i("Sync canceled, connection not available")
showWarningNotification(getString(R.string.notification_no_connection_title),
getString(R.string.notification_no_connection_body))
// Get connectivity status
val connection = ReactiveNetwork().getConnectivityStatus(this, true)
// Get library update restrictions
val restrictions = preferences.libraryUpdateRestriction()
// Check if users updates library manual
val isManualUpdate = intent?.getBooleanExtra(UPDATE_IS_MANUAL, false) ?: false
// Whether to cancel the update.
var cancelUpdate = false
// Check if device has internet connection
// Check if device has wifi connection if only wifi is enabled
if (connection == ConnectivityStatus.OFFLINE || ("wifi" in restrictions
&& connection != ConnectivityStatus.WIFI_CONNECTED_HAS_INTERNET)) {
if (isManualUpdate) {
toast(R.string.notification_no_connection_title)
}
// Enable library update when connection available
AndroidComponentUtil.toggleComponent(this, SyncOnConnectionAvailable::class.java, true)
cancelUpdate = true
}
if (!isManualUpdate && "ac" in restrictions && !DeviceUtil.isPowerConnected(this)) {
AndroidComponentUtil.toggleComponent(this, SyncOnPowerConnected::class.java, true)
cancelUpdate = true
}
if (cancelUpdate) {
stopSelf(startId)
return Service.START_NOT_STICKY
}
// If user doesn't want to update while phone is not charging, cancel sync
else if (preferences.updateOnlyWhenCharging() && !(intent?.getBooleanExtra(UPDATE_IS_FORCED, false) ?: false) && !DeviceUtil.isPowerConnected(this)) {
Timber.i("Sync canceled, not connected to ac power")
// 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
}
// Stop enabled components.
AndroidComponentUtil.toggleComponent(this, SyncOnConnectionAvailable::class.java, false)
AndroidComponentUtil.toggleComponent(this, SyncOnPowerConnected::class.java, false)
// Unsubscribe from any previous subscription if needed.
subscription?.unsubscribe()
@ -168,20 +182,11 @@ class LibraryUpdateService : Service() {
stopSelf(startId)
}, {
stopSelf(startId)
})
})
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
* heavy operations or network calls here.
@ -195,7 +200,8 @@ class LibraryUpdateService : Service() {
val newUpdates = 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.
val allLibraryMangas = db.getFavoriteMangas().executeAsBlocking()
@ -299,12 +305,11 @@ class LibraryUpdateService : Service() {
* @param body the body of the notification.
*/
private fun showNotification(title: String, body: String) {
val n = notification() {
notificationManager.notify(UPDATE_NOTIFICATION_ID, notification() {
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
setContentTitle(title)
setContentText(body)
}
notificationManager.notify(UPDATE_NOTIFICATION_ID, n)
})
}
/**
@ -314,35 +319,15 @@ class LibraryUpdateService : Service() {
* @param total the total progress.
*/
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)
setContentTitle(manga.title)
setProgress(total, current, false)
setOngoing(true)
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.
@ -353,14 +338,13 @@ class LibraryUpdateService : Service() {
val title = getString(R.string.notification_update_completed)
val body = getUpdatedMangasBody(updates, failed)
val n = notification() {
notificationManager.notify(UPDATE_NOTIFICATION_ID, notification() {
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
setContentTitle(title)
setStyle(NotificationCompat.BigTextStyle().bigText(body))
setContentIntent(notificationIntent)
setAutoCancel(true)
}
notificationManager.notify(UPDATE_NOTIFICATION_ID, n)
})
}
/**
@ -385,7 +369,6 @@ class LibraryUpdateService : Service() {
* network changes.
*/
class SyncOnConnectionAvailable : BroadcastReceiver() {
/**
* Method called when a network change occurs.
* @param context the application context.
@ -394,35 +377,38 @@ class LibraryUpdateService : Service() {
override fun onReceive(context: Context, intent: Intent) {
if (DeviceUtil.isNetworkConnected(context)) {
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() {
companion object {
// Cancel library update action
val CANCEL_LIBRARY_UPDATE = "eu.kanade.CANCEL_LIBRARY_UPDATE"
// Force library update
val FORCE_LIBRARY_UPDATE = "eu.kanade.FORCE_LIBRARY_UPDATE"
class SyncOnPowerConnected: BroadcastReceiver() {
/**
* Method called when AC is connected.
* @param context the application context.
* @param intent the intent received.
*/
override fun onReceive(context: Context, intent: Intent) {
AndroidComponentUtil.toggleComponent(context, this.javaClass, false)
start(context)
}
}
/**
* Class that stops updating the library.
*/
class CancelUpdateReceiver : BroadcastReceiver() {
/**
* Method called when user wants a library update.
* @param context the application context.
* @param intent the intent received.
*/
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
CANCEL_LIBRARY_UPDATE -> {
LibraryUpdateService.stop(context)
context.notificationManager.cancel(UPDATE_NOTIFICATION_ID)
}
FORCE_LIBRARY_UPDATE -> LibraryUpdateService.start(context, true)
}
LibraryUpdateService.stop(context)
context.notificationManager.cancel(UPDATE_NOTIFICATION_ID)
}
}

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>
open val isLogged: Boolean
get() = !preferences.getMangaSyncUsername(this).isEmpty() &&
!preferences.getMangaSyncPassword(this).isEmpty()
get() = !preferences.mangaSyncUsername(this).isEmpty() &&
!preferences.mangaSyncPassword(this).isEmpty()
abstract fun update(manga: MangaSync): Observable<Response>

View File

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

View File

@ -10,10 +10,13 @@ import java.net.CookieStore
class NetworkHelper(context: Context) {
private val client: OkHttpClient
private val forceCacheClient: OkHttpClient
private val cacheDir = File(context.cacheDir, "network_cache")
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 ->
val originalResponse = chain.proceed(chain.request())
@ -23,56 +26,51 @@ class NetworkHelper(context: Context) {
.build()
}
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
private val cacheDir = "network_cache"
private val client = OkHttpClient.Builder()
.cookieJar(JavaNetCookieJar(cookieManager))
.cache(Cache(cacheDir, cacheSize))
.build()
init {
val cacheDir = File(context.cacheDir, cacheDir)
private val forceCacheClient = client.newBuilder()
.addNetworkInterceptor(forceCacheInterceptor)
.build()
cookieManager = CookieManager()
cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL)
client = OkHttpClient.Builder()
.cookieJar(JavaNetCookieJar(cookieManager))
.cache(Cache(cacheDir, cacheSize))
.build()
forceCacheClient = client.newBuilder()
.addNetworkInterceptor(forceCacheInterceptor)
.build()
}
val cookies: CookieStore
get() = cookieManager.cookieStore
@JvmOverloads
fun request(request: Request, forceCache: Boolean = false): Observable<Response> {
return Observable.fromCallable {
val c = if (forceCache) forceCacheClient else client
c.newCall(request).execute()
c.newCall(request).execute().apply { body().close() }
}
}
@JvmOverloads
fun requestBody(request: Request, forceCache: Boolean = false): Observable<String> {
return request(request, forceCache)
.map { it.body().string() }
return Observable.fromCallable {
val c = if (forceCache) forceCacheClient else client
c.newCall(request).execute().body().string()
}
}
fun requestBodyProgress(request: Request, listener: ProgressListener): Observable<Response> {
return Observable.fromCallable {
val progressClient = client.newBuilder()
.cache(null)
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.body(ProgressResponseBody(originalResponse.body(), listener))
.build()
}
.build()
progressClient.newCall(request).execute()
}.retry(1)
return Observable.fromCallable { requestBodyProgressBlocking(request, listener) }
}
fun requestBodyProgressBlocking(request: Request, listener: ProgressListener): Response {
val progressClient = client.newBuilder()
.cache(null)
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.body(ProgressResponseBody(originalResponse.body(), listener))
.build()
}
.build()
return progressClient.newCall(request).execute()
}
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) {
val keys = PreferenceKeys(context)
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
private val rxPrefs = RxSharedPreferences.create(prefs)
private val defaultDownloadsDir: File
private val defaultDownloadsDir = File(Environment.getExternalStorageDirectory().absolutePath +
File.separator + context.getString(R.string.app_name), "downloads")
init {
defaultDownloadsDir = File(Environment.getExternalStorageDirectory().absolutePath +
File.separator + context.getString(R.string.app_name), "downloads")
// Don't display downloaded chapters in gallery apps creating a ".nomedia" file
try {
File(downloadsDirectory, ".nomedia").createNewFile()
File(downloadsDirectory().getOrDefault(), ".nomedia").createNewFile()
} catch (e: IOException) {
/* Ignore */
}
}
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 {
return PreferenceManager.getDefaultSharedPreferences(context).getInt(
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
fun getTheme(context: Context): Int {
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() {
prefs.edit().clear().apply()
}
fun rotation(): Preference<Int> {
return rxPrefs.getInteger(getKey(R.string.pref_rotation_type_key), 1)
}
fun rotation() = rxPrefs.getInteger(keys.rotation, 1)
fun enableTransitions(): Preference<Boolean> {
return rxPrefs.getBoolean(getKey(R.string.pref_enable_transitions_key), true)
}
fun enableTransitions() = rxPrefs.getBoolean(keys.enableTransitions, true)
fun showPageNumber(): Preference<Boolean> {
return rxPrefs.getBoolean(getKey(R.string.pref_show_page_number_key), true)
}
fun showPageNumber() = rxPrefs.getBoolean(keys.showPageNumber, true)
fun hideStatusBar(): Preference<Boolean> {
return rxPrefs.getBoolean(getKey(R.string.pref_hide_status_bar_key), true)
}
fun hideStatusBar() = rxPrefs.getBoolean(keys.hideStatusBar, true)
fun keepScreenOn(): Preference<Boolean> {
return rxPrefs.getBoolean(getKey(R.string.pref_keep_screen_on_key), true)
}
fun keepScreenOn() = rxPrefs.getBoolean(keys.keepScreenOn, true)
fun customBrightness(): Preference<Boolean> {
return rxPrefs.getBoolean(getKey(R.string.pref_custom_brightness_key), false)
}
fun customBrightness() = rxPrefs.getBoolean(keys.customBrightness, false)
fun customBrightnessValue(): Preference<Float> {
return rxPrefs.getFloat(getKey(R.string.pref_custom_brightness_value_key), 0f)
}
fun customBrightnessValue() = rxPrefs.getFloat(keys.customBrightnessValue, 0f)
val defaultViewer: Int
get() = prefs.getInt(getKey(R.string.pref_default_viewer_key), 1)
fun defaultViewer() = prefs.getInt(keys.defaultViewer, 1)
fun imageScaleType(): Preference<Int> {
return rxPrefs.getInteger(getKey(R.string.pref_image_scale_type_key), 1)
}
fun imageScaleType() = rxPrefs.getInteger(keys.imageScaleType, 1)
fun imageDecoder(): Preference<Int> {
return rxPrefs.getInteger(getKey(R.string.pref_image_decoder_key), 0)
}
fun imageDecoder() = rxPrefs.getInteger(keys.imageDecoder, 0)
fun zoomStart(): Preference<Int> {
return rxPrefs.getInteger(getKey(R.string.pref_zoom_start_key), 1)
}
fun zoomStart() = rxPrefs.getInteger(keys.zoomStart, 1)
fun readerTheme(): Preference<Int> {
return rxPrefs.getInteger(getKey(R.string.pref_reader_theme_key), 0)
}
fun readerTheme() = rxPrefs.getInteger(keys.readerTheme, 0)
fun portraitColumns(): Preference<Int> {
return rxPrefs.getInteger(getKey(R.string.pref_library_columns_portrait_key), 0)
}
fun readWithTapping() = rxPrefs.getBoolean(keys.readWithTapping, true)
fun landscapeColumns(): Preference<Int> {
return rxPrefs.getInteger(getKey(R.string.pref_library_columns_landscape_key), 0)
}
fun readWithVolumeKeys() = rxPrefs.getBoolean(keys.readWithVolumeKeys, false)
fun updateOnlyNonCompleted(): Boolean {
return prefs.getBoolean(getKey(R.string.pref_update_only_non_completed_key), false)
}
fun reencodeImage() = prefs.getBoolean(keys.reencodeImage, false)
fun autoUpdateMangaSync(): Boolean {
return prefs.getBoolean(getKey(R.string.pref_auto_update_manga_sync_key), true)
}
fun portraitColumns() = rxPrefs.getInteger(keys.portraitColumns, 0)
fun askUpdateMangaSync(): Boolean {
return prefs.getBoolean(getKey(R.string.pref_ask_update_manga_sync_key), false)
}
fun landscapeColumns() = rxPrefs.getInteger(keys.landscapeColumns, 0)
fun lastUsedCatalogueSource(): Preference<Int> {
return rxPrefs.getInteger(getKey(R.string.pref_last_catalogue_source_key), -1)
}
fun updateOnlyNonCompleted() = prefs.getBoolean(keys.updateOnlyNonCompleted, false)
fun seamlessMode(): Boolean {
return prefs.getBoolean(getKey(R.string.pref_seamless_mode_key), true)
}
fun autoUpdateMangaSync() = prefs.getBoolean(keys.autoUpdateMangaSync, true)
fun catalogueAsList(): Preference<Boolean> {
return rxPrefs.getBoolean(getKey(R.string.pref_display_catalogue_as_list), false)
}
fun askUpdateMangaSync() = prefs.getBoolean(keys.askUpdateMangaSync, false)
fun enabledLanguages(): Preference<MutableSet<String>> {
return rxPrefs.getStringSet(getKey(R.string.pref_source_languages), setOf("EN"))
}
fun lastUsedCatalogueSource() = rxPrefs.getInteger(keys.lastUsedCatalogueSource, -1)
fun getSourceUsername(source: Source): String {
return prefs.getString(SOURCE_ACCOUNT_USERNAME + source.id, "")
}
fun seamlessMode() = prefs.getBoolean(keys.seamlessMode, true)
fun getSourcePassword(source: Source): String {
return prefs.getString(SOURCE_ACCOUNT_PASSWORD + source.id, "")
}
fun catalogueAsList() = rxPrefs.getBoolean(keys.catalogueAsList, false)
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) {
prefs.edit()
.putString(SOURCE_ACCOUNT_USERNAME + source.id, username)
.putString(SOURCE_ACCOUNT_PASSWORD + source.id, password)
.putString(keys.sourceUsername(source.id), username)
.putString(keys.sourcePassword(source.id), password)
.apply()
}
fun getMangaSyncUsername(sync: MangaSyncService): String {
return prefs.getString(MANGASYNC_ACCOUNT_USERNAME + sync.id, "")
}
fun mangaSyncUsername(sync: MangaSyncService) = prefs.getString(keys.syncUsername(sync.id), "")
fun getMangaSyncPassword(sync: MangaSyncService): String {
return prefs.getString(MANGASYNC_ACCOUNT_PASSWORD + sync.id, "")
}
fun mangaSyncPassword(sync: MangaSyncService) = prefs.getString(keys.syncPassword(sync.id), "")
fun setMangaSyncCredentials(sync: MangaSyncService, username: String, password: String) {
prefs.edit()
.putString(MANGASYNC_ACCOUNT_USERNAME + sync.id, username)
.putString(MANGASYNC_ACCOUNT_PASSWORD + sync.id, password)
.putString(keys.syncUsername(sync.id), username)
.putString(keys.syncPassword(sync.id), password)
.apply()
}
var downloadsDirectory: String
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 downloadsDirectory() = rxPrefs.getString(keys.downloadsDirectory, defaultDownloadsDir.absolutePath)
fun downloadThreads(): Preference<Int> {
return rxPrefs.getInteger(getKey(R.string.pref_download_slots_key), 1)
}
fun downloadThreads() = rxPrefs.getInteger(keys.downloadThreads, 1)
fun downloadOnlyOverWifi(): Boolean {
return prefs.getBoolean(getKey(R.string.pref_download_only_over_wifi_key), true)
}
fun downloadOnlyOverWifi() = prefs.getBoolean(keys.downloadOnlyOverWifi, true)
fun removeAfterRead(): Boolean {
return prefs.getBoolean(getKey(R.string.pref_remove_after_read_key), false)
}
fun removeAfterRead() = prefs.getBoolean(keys.removeAfterRead, false)
fun removeAfterReadPrevious(): Boolean {
return prefs.getBoolean(getKey(R.string.pref_remove_after_read_previous_key), false)
}
fun removeAfterReadPrevious() = prefs.getBoolean(keys.removeAfterReadPrevious, false)
fun removeAfterMarkedAsRead(): Boolean {
return prefs.getBoolean(getKey(R.string.pref_remove_after_marked_as_read_key), false)
}
fun removeAfterMarkedAsRead() = prefs.getBoolean(keys.removeAfterMarkedAsRead, false)
fun updateOnlyWhenCharging(): Boolean {
return prefs.getBoolean(getKey(R.string.pref_update_only_when_charging_key), false)
}
fun libraryUpdateInterval() = rxPrefs.getInteger(keys.libraryUpdateInterval, 0)
fun libraryUpdateInterval(): Preference<Int> {
return rxPrefs.getInteger(getKey(R.string.pref_library_update_interval_key), 0)
}
fun libraryUpdateRestriction() = prefs.getStringSet(keys.libraryUpdateRestriction, emptySet())
fun filterDownloaded(): Preference<Boolean> {
return rxPrefs.getBoolean(getKey(R.string.pref_filter_downloaded_key), false)
}
fun filterDownloaded() = rxPrefs.getBoolean(keys.filterDownloaded, false)
fun filterUnread(): Preference<Boolean> {
return rxPrefs.getBoolean(getKey(R.string.pref_filter_unread_key), false)
}
fun filterUnread() = rxPrefs.getBoolean(keys.filterUnread, false)
}

View File

@ -1,8 +1,8 @@
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 RU = Language("Russian", "RU")
val EN = Language("EN", "English")
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
return getImageProgressResponse(page)
.flatMap { resp ->
chapterCache.putImageToCache(page.imageUrl, resp)
chapterCache.putImageToCache(page.imageUrl, resp, prefs.reencodeImage())
Observable.just(page)
}
}
open fun getImageProgressResponse(page: Page): Observable<Response> {
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>?) {

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.data.source.model;
import java.io.Serializable;
import java.util.List;
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 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) {
this.pageNumber = pageNumber;
this.url = url;
@ -32,10 +39,6 @@ public class Page implements ProgressListener {
this.imagePath = imagePath;
}
public Page(int pageNumber, String url) {
this(pageNumber, url, null, null);
}
public int getPageNumber() {
return pageNumber;
}

View File

@ -372,8 +372,8 @@ public class Batoto extends LoginSource {
@Override
public Observable<List<Chapter>> pullChaptersFromNetwork(final String mangaUrl) {
Observable<List<Chapter>> observable;
String username = getPrefs().getSourceUsername(this);
String password = getPrefs().getSourcePassword(this);
String username = getPrefs().sourceUsername(this);
String password = getPrefs().sourcePassword(this);
if (username.isEmpty() && password.isEmpty()) {
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>>) {
fun getMangasForCategory(category: Category): List<Manga>? {
fun getMangaForCategory(category: Category): List<Manga>? {
return mangas[category.id]
}
}

View File

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

View File

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

View File

@ -11,7 +11,6 @@ import nucleus.presenter.Presenter;
import nucleus.view.PresenterLifecycleDelegate;
import nucleus.view.ViewWithPresenter;
/**
* 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
@ -87,8 +86,9 @@ public abstract class BaseRxActivity<P extends Presenter> extends BaseActivity i
}
@Override
protected void onPause() {
super.onPause();
presenterDelegate.onPause(isFinishing());
protected void onDestroy() {
super.onDestroy();
presenterDelegate.onDropView();
presenterDelegate.onDestroy(!isChangingConfigurations());
}
}

View File

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

View File

@ -1,10 +1,14 @@
package eu.kanade.tachiyomi.ui.catalogue
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.RelativeLayout
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.util.inflate
import kotlinx.android.synthetic.main.fragment_catalogue.*
import kotlinx.android.synthetic.main.item_catalogue_grid.view.*
import java.util.*
/**
@ -68,6 +72,7 @@ class CatalogueAdapter(val fragment: CatalogueFragment) : FlexibleAdapter<Catalo
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CatalogueHolder {
if (parent.id == R.id.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)
} else {
val v = parent.inflate(R.layout.item_catalogue_list)
@ -86,4 +91,10 @@ class CatalogueAdapter(val fragment: CatalogueFragment) : FlexibleAdapter<Catalo
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
import android.content.res.Configuration
import android.os.Bundle
import android.support.v7.widget.GridLayoutManager
import android.support.v7.widget.SearchView
@ -11,6 +12,7 @@ import android.widget.ArrayAdapter
import android.widget.ProgressBar
import android.widget.Spinner
import com.afollestad.materialdialogs.MaterialDialog
import com.f2prateek.rx.preferences.Preference
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
@ -31,7 +33,7 @@ import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.subjects.PublishSubject
import timber.log.Timber
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeUnit.MILLISECONDS
/**
* Fragment that shows the manga from the catalogue.
@ -63,7 +65,8 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
/**
* Query of the search box.
*/
private var query = ""
private val query: String?
get() = presenter.query
/**
* Selected index of the spinner (selected source).
@ -85,6 +88,11 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
*/
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).
*/
@ -102,23 +110,11 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
get() = (activity as MainActivity).toolbar
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.
*
* @return a new instance of [CatalogueFragment].
*/
@JvmStatic
fun newInstance(): CatalogueFragment {
return CatalogueFragment()
}
@ -127,13 +123,6 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
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? {
@ -162,6 +151,12 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
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.outAnimation = AnimationUtils.loadAnimation(activity, android.R.anim.fade_out)
@ -175,19 +170,15 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
val onItemSelected = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
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)) {
spinner.setSelection(presenter.findFirstValidSource())
context.toast(R.string.source_requires_login)
} else {
selectedIndex = position
presenter.setEnabledSource(selectedIndex)
showProgressBar()
glm.scrollToPositionWithOffset(0, 0)
llm.scrollToPositionWithOffset(0, 0)
presenter.startRequesting(source)
}
if (!presenter.isValidSource(source)) {
spinner.setSelection(selectedIndex)
context.toast(R.string.source_requires_login)
} else if (source != presenter.source) {
selectedIndex = position
showProgressBar()
glm.scrollToPositionWithOffset(0, 0)
llm.scrollToPositionWithOffset(0, 0)
presenter.setActiveSource(source)
}
}
@ -197,18 +188,15 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
spinner = Spinner(themedContext).apply {
adapter = spinnerAdapter
selectedIndex = presenter.sources.indexOf(presenter.source)
setSelection(selectedIndex)
onItemSelectedListener = onItemSelected
}
setToolbarTitle("")
toolbar.addView(spinner)
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putInt(SELECTED_INDEX_KEY, selectedIndex)
outState.putString(QUERY_KEY, query)
super.onSaveInstanceState(outState)
showProgressBar()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@ -255,17 +243,20 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
return true
}
override fun onStart() {
super.onStart()
initializeSearchSubscription()
override fun onResume() {
super.onResume()
queryDebouncerSubscription = queryDebouncerSubject.debounce(SEARCH_TIMEOUT, MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { searchWithQuery(it) }
}
override fun onStop() {
destroySearchSubscription()
super.onStop()
override fun onPause() {
queryDebouncerSubscription?.unsubscribe()
super.onPause()
}
override fun onDestroyView() {
numColumnsSubscription?.unsubscribe()
searchItem?.let {
if (it.isActionViewExpanded) it.collapseActionView()
}
@ -274,51 +265,34 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
}
/**
* Listen for query events on the debouncer.
*/
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
* Called when the input text changes or is submitted.
*
* @param query the new query.
* @param now whether to send the network call now or debounce it by [SEARCH_TIMEOUT].
*/
private fun onSearchEvent(query: String, now: Boolean) {
if (now) {
restartRequest(query)
searchWithQuery(query)
} else {
queryDebouncerSubject.onNext(query)
}
}
/**
* Restarts the request.
* Restarts the request with a 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 (query == newQuery)
return
query = newQuery
showProgressBar()
catalogue_grid.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 ?: "") {
setAction(R.string.action_retry) {
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.
*
@ -432,8 +418,7 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
override fun onListItemClick(position: Int): Boolean {
val item = adapter.getItem(position) ?: return false
val intent = MangaActivity.newIntent(activity, item)
intent.putExtra(MangaActivity.FROM_CATALOGUE, true)
val intent = MangaActivity.newIntent(activity, item, true)
startActivity(intent)
return false
}
@ -444,16 +429,16 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
* @param position the position of the element clicked.
*/
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)
.items(getString(textRes))
.itemsCallback { dialog, itemView, which, text ->
when (which) {
0 -> {
presenter.changeMangaFavorite(selectedManga)
presenter.changeMangaFavorite(manga)
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.preference.PreferencesHelper
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.base.Source
import eu.kanade.tachiyomi.data.source.model.MangasPage
@ -51,12 +52,13 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
/**
* Query from the view.
*/
private var query: String? = null
var query = ""
private set
/**
* Pager containing a list of manga results.
*/
private lateinit var pager: RxPager<Manga>
private var pager = RxPager<Manga>()
/**
* Last fetched page from network.
@ -76,45 +78,36 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
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?) {
super.onCreate(savedState)
source = getLastUsedSource()
if (savedState != null) {
source = sourceManager.get(savedState.getInt(ACTIVE_SOURCE_KEY))!!
query = savedState.getString(QUERY_KEY, "")
}
pager = RxPager()
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,
startableLatestCache(GET_MANGA_DETAILS,
{ mangaDetailSubject.observeOn(Schedulers.io())
.flatMap { Observable.from(it) }
.filter { !it.initialized }
@ -126,10 +119,22 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
add(prefs.catalogueAsList().asObservable()
.subscribe { setDisplayMode(it) })
startableReplay(PAGER,
{ pager.results() },
{ view, pair -> view.onAddPage(pair.first, pair.second) })
startableFirst(REQUEST_PAGE,
{ pager.request { page -> getMangasPageObservable(page + 1) } },
{ view, next -> },
{ view, error -> view.onAddPageError(error) })
start(PAGER)
start(REQUEST_PAGE)
}
override fun onSave(state: Bundle) {
state.putInt(ACTIVE_SOURCE_KEY, source.id)
state.putString(QUERY_KEY, query)
super.onSave(state)
}
@ -141,37 +146,38 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
private fun setDisplayMode(asList: Boolean) {
isListMode = asList
if (asList) {
stop(GET_MANGA_DETAIL)
stop(GET_MANGA_DETAILS)
} 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
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
stop(GET_MANGA_PAGE)
stop(REQUEST_PAGE)
lastMangasPage = null
if (!isListMode) {
start(GET_MANGA_DETAIL)
start(GET_MANGA_DETAILS)
}
start(GET_MANGA_LIST)
start(GET_MANGA_PAGE)
start(PAGER)
start(REQUEST_PAGE)
}
/**
@ -179,15 +185,22 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
*/
fun requestNext() {
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() {
start(GET_MANGA_PAGE)
fun hasNextPage(): Boolean {
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
}
val obs = if (query.isNullOrEmpty())
val observable = if (query.isEmpty())
source.pullPopularMangasFromNetwork(nextMangasPage)
else
source.searchMangasFromNetwork(nextMangasPage, query!!)
source.searchMangasFromNetwork(nextMangasPage, query)
return obs.subscribeOn(Schedulers.io())
return observable.subscribeOn(Schedulers.io())
.doOnNext { lastMangasPage = it }
.flatMap { Observable.from(it.mangas) }
.map { networkToLocalManga(it) }
@ -259,23 +272,17 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
}
/**
* Returns true if the last fetched page has a next page.
*/
fun hasNextPage(): Boolean {
return lastMangasPage?.nextPageUrl != null
}
/**
* Gets the last used source from preferences, or the first valid source.
* Returns 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 {
val index = prefs.lastUsedCatalogueSource().get() ?: -1
if (index < 0 || index >= sources.size || !isValidSource(sources[index])) {
fun getLastUsedSource(): Source {
val id = prefs.lastUsedCatalogueSource().get() ?: -1
val source = sourceManager.get(id)
if (!isValidSource(source)) {
return findFirstValidSource()
}
return index
return source!!
}
/**
@ -284,11 +291,16 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
* @param source the source to check.
* @return true if the source is valid, false otherwise.
*/
fun isValidSource(source: Source): Boolean = with(source) {
if (!isLoginRequired || isLogged)
return true
fun isValidSource(source: Source?): Boolean {
if (source == null) return false
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.
*/
fun findFirstValidSource(): Int {
return sources.indexOfFirst { isValidSource(it) }
}
/**
* Sets the enabled source.
*
* @param index the index of the source in [sources].
*/
fun setEnabledSource(index: Int) {
prefs.lastUsedCatalogueSource().set(index)
fun findFirstValidSource(): Source {
return sources.find { isValidSource(it) }!!
}
/**
@ -317,7 +320,7 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
// Ensure at least one language
if (languages.isEmpty()) {
languages.add("EN")
languages.add(EN.code)
}
return sourceManager.getSources()

View File

@ -10,7 +10,13 @@ import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.widget.NpaLinearLayoutManager
import kotlinx.android.synthetic.main.fragment_download_queue.*
import nucleus.factory.RequiresPresenter
import rx.Observable
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.
@ -40,9 +46,14 @@ class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
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.
@ -66,8 +77,7 @@ class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
setHasOptionsMenu(true)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedState: Bundle?): View? {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View {
return inflater.inflate(R.layout.fragment_download_queue, container, false)
}
@ -123,15 +133,91 @@ class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
override fun onResume() {
super.onResume()
queueStatusSubscription = presenter.downloadManager.runningSubject
presenter.downloadManager.runningSubject
.observeOn(AndroidSchedulers.mainThread())
.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() {
queueStatusSubscription?.unsubscribe()
for (subscription in progressSubscriptions.values) {
subscription.unsubscribe()
}
progressSubscriptions.clear()
resumeSubscriptions.clear()
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.
*
@ -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) {
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.
*/

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.
*
* @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) {

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.ui.base.presenter.BasePresenter
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import timber.log.Timber
import java.util.*
import java.util.concurrent.TimeUnit
import javax.inject.Inject
/**
@ -19,6 +14,13 @@ import javax.inject.Inject
*/
class DownloadPresenter : BasePresenter<DownloadFragment>() {
companion object {
/**
* Id of the restartable that returns the download queue.
*/
const val GET_DOWNLOAD_QUEUE = 1
}
/**
* Download manager.
*/
@ -30,28 +32,6 @@ class DownloadPresenter : BasePresenter<DownloadFragment>() {
val downloadQueue: DownloadQueue
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?) {
super.onCreate(savedState)
@ -65,102 +45,14 @@ class DownloadPresenter : BasePresenter<DownloadFragment>() {
}
}
override fun onTakeView(view: DownloadFragment) {
super.onTakeView(view)
statusSubscription = downloadQueue.getStatusObservable()
fun getStatusObservable(): Observable<Download> {
return downloadQueue.getStatusObservable()
.startWith(downloadQueue.getActiveDownloads())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { processStatus(it, view) }
}
add(statusSubscription)
pageProgressSubscription = downloadQueue.getProgressObservable()
fun getProgressObservable(): Observable<Download> {
return downloadQueue.getProgressObservable()
.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 kotlinx.android.synthetic.main.fragment_library_category.*
import rx.Subscription
import java.util.*
/**
* Fragment containing the library manga for a certain category.
@ -34,14 +33,6 @@ class LibraryCategoryFragment : BaseFragment(), FlexibleViewHolder.OnListItemCli
*/
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.
*/
@ -119,7 +110,7 @@ class LibraryCategoryFragment : BaseFragment(), FlexibleViewHolder.OnListItemCli
override fun onResume() {
super.onResume()
libraryMangaSubscription = libraryPresenter.libraryMangaSubject
.subscribe { if (it != null) onNextLibraryManga(it) }
.subscribe { onNextLibraryManga(it) }
}
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
* and refresh the content of the adapter.
* Subscribe to [LibraryMangaEvent]. When an event is received, it updates the content of the
* adapter.
*
* @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.
if (position >= categories.size) return
// Get the manga list for this category
val mangaForCategory = event.getMangasForCategory(categories[position])
// Get the manga list for this category.
val mangaForCategory = event.getMangaForCategory(categories[position]) ?: emptyList()
// Update the list only if the reference to the list is different, avoiding reseting the
// adapter after every onResume.
if (mangas !== mangaForCategory) {
mangas = mangaForCategory
adapter.setItems(mangas ?: emptyList())
}
// Update the category with its manga.
adapter.setItems(mangaForCategory)
}
/**

View File

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

View File

@ -44,7 +44,7 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
/**
* Subject to notify the library's viewpager for updates.
*/
val libraryMangaSubject = BehaviorSubject.create<LibraryMangaEvent?>()
val libraryMangaSubject = BehaviorSubject.create<LibraryMangaEvent>()
/**
* 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.
*
@ -114,13 +102,6 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
.observeOn(AndroidSchedulers.mainThread())
}
/**
* Update the library information
*/
fun updateLibrary() {
start(GET_LIBRARY)
}
/**
* 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
*
@ -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> {
return categories.map { it.name }
}
fun getCommonCategories(mangas: List<Manga>) = mangas.toSet()
.map { db.getCategoriesForManga(it).executeAsBlocking() }
.reduce { set1: Iterable<Category>, set2 -> set1.intersect(set2) }
/**
* Remove the selected manga from the library.

View File

@ -39,7 +39,7 @@ class MainActivity : BaseActivity() {
supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_menu_white_24dp)
if (Build.VERSION.SDK_INT >= 21) {
window.statusBarColor = android.R.color.transparent;
window.statusBarColor = android.R.color.transparent
}
// Set behavior of Navigation drawer
@ -73,6 +73,16 @@ class MainActivity : BaseActivity() {
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) {
supportFragmentManager.beginTransaction()
.replace(R.id.frame_container, fragment)

View File

@ -23,21 +23,24 @@ class MangaActivity : BaseRxActivity<MangaPresenter>() {
companion object {
val FROM_CATALOGUE = "from_catalogue"
val INFO_FRAGMENT = 0
val CHAPTERS_FRAGMENT = 1
val MYANIMELIST_FRAGMENT = 2
const val FROM_CATALOGUE_EXTRA = "from_catalogue"
const val MANGA_EXTRA = "manga"
const val INFO_FRAGMENT = 0
const val CHAPTERS_FRAGMENT = 1
const val MYANIMELIST_FRAGMENT = 2
fun newIntent(context: Context, manga: Manga): Intent {
val intent = Intent(context, MangaActivity::class.java)
fun newIntent(context: Context, manga: Manga, fromCatalogue: Boolean = false): Intent {
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
var isCatalogueManga: Boolean = false
var fromCatalogue: Boolean = false
private set
override fun onCreate(savedState: Bundle?) {
@ -45,16 +48,21 @@ class MangaActivity : BaseRxActivity<MangaPresenter>() {
super.onCreate(savedState)
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)
isCatalogueManga = intent.getBooleanExtra(FROM_CATALOGUE, false)
fromCatalogue = intent.getBooleanExtra(FROM_CATALOGUE_EXTRA, false)
adapter = MangaDetailAdapter(supportFragmentManager, this)
view_pager.adapter = adapter
tabs.setupWithViewPager(view_pager)
if (!isCatalogueManga)
if (!fromCatalogue)
view_pager.currentItem = CHAPTERS_FRAGMENT
requestPermissionsOnMarshmallow()
@ -72,7 +80,7 @@ class MangaActivity : BaseRxActivity<MangaPresenter>() {
init {
pageCount = 2
if (!activity.isCatalogueManga && activity.presenter.syncManager.myAnimeList.isLogged)
if (!activity.fromCatalogue && activity.presenter.syncManager.myAnimeList.isLogged)
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.util.SharedData
import rx.Observable
import rx.Subscription
import javax.inject.Inject
/**
@ -31,37 +32,21 @@ class MangaPresenter : BasePresenter<MangaActivity>() {
*/
lateinit var manga: Manga
/**
* Key to save and restore [manga] from a bundle.
*/
private val MANGA_KEY = "manga_key"
var mangaSubscription: Subscription? = null
override fun onCreate(savedState: Bundle?) {
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.
SharedData.put(ChapterCountEvent())
Observable.just(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)
fun setMangaEvent(event: MangaEvent) {
if (isUnsubscribed(mangaSubscription)) {
manga = event.manga
mangaSubscription = Observable.just(manga)
.subscribeLatestCache({ view, manga -> view.onSetManga(manga) })
}
}
}

View File

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

View File

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

View File

@ -98,38 +98,40 @@ class MangaInfoFragment : BaseRxFragment<MangaInfoPresenter>() {
val coverCache = presenter.coverCache
val headers = presenter.source.glideHeaders
// Check if thumbnail_url is given.
manga.thumbnail_url?.let { url ->
if (manga.favorite) {
coverCache.saveOrLoadFromCache(url, headers) {
if (isResumed) {
Glide.with(context)
.load(it)
.diskCacheStrategy(DiskCacheStrategy.RESULT)
.centerCrop()
.signature(StringSignature(it.lastModified().toString()))
.into(manga_cover)
// Set cover if it wasn't already.
if (manga_cover.drawable == null) {
manga.thumbnail_url?.let { url ->
if (manga.favorite) {
coverCache.saveOrLoadFromCache(url, headers) {
if (isResumed) {
Glide.with(context)
.load(it)
.diskCacheStrategy(DiskCacheStrategy.RESULT)
.centerCrop()
.signature(StringSignature(it.lastModified().toString()))
.into(manga_cover)
Glide.with(context)
.load(it)
.diskCacheStrategy(DiskCacheStrategy.RESULT)
.centerCrop()
.signature(StringSignature(it.lastModified().toString()))
.into(backdrop)
Glide.with(context)
.load(it)
.diskCacheStrategy(DiskCacheStrategy.RESULT)
.centerCrop()
.signature(StringSignature(it.lastModified().toString()))
.into(backdrop)
}
}
}
} else {
Glide.with(context)
.load(if (headers != null) GlideUrl(url, headers) else url)
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
.centerCrop()
.into(manga_cover)
} else {
Glide.with(context)
.load(if (headers != null) GlideUrl(url, headers) else url)
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
.centerCrop()
.into(manga_cover)
Glide.with(context)
.load(if (headers != null) GlideUrl(url, headers) else url)
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
.centerCrop()
.into(backdrop)
Glide.with(context)
.load(if (headers != null) GlideUrl(url, headers) else url)
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
.centerCrop()
.into(backdrop)
}
}
}
}

View File

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

View File

@ -56,6 +56,10 @@ class MyAnimeListFragment : BaseRxFragment<MyAnimeListPresenter>() {
myanimelist_status.text = presenter.myAnimeList.getStatus(it.status)
} ?: run {
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,20 +123,24 @@ class MyAnimeListPresenter : BasePresenter<MyAnimeListFragment>() {
stop(GET_SEARCH_RESULTS)
}
fun registerManga(sync: MangaSync) {
sync.manga_id = manga.id
add(myAnimeList.bind(sync)
.flatMap { response ->
if (response.isSuccessful) {
db.insertMangaSync(sync).asRxObservable()
} else {
Observable.error(Exception("Could not bind manga"))
fun registerManga(sync: MangaSync?) {
if (sync != null) {
sync.manga_id = manga.id
add(myAnimeList.bind(sync)
.flatMap { response ->
if (response.isSuccessful) {
db.insertMangaSync(sync).asRxObservable()
} else {
Observable.error(Exception("Could not bind manga"))
}
}
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ },
{ error -> context.toast(error.message) }))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ },
{ error -> context.toast(error.message) }))
} else {
db.deleteMangaSyncForManga(manga).executeAsBlocking()
}
}
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.getOrDefault
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.listener.SimpleAnimationListener
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.webtoon.WebtoonReader
import eu.kanade.tachiyomi.util.GLUtil
import eu.kanade.tachiyomi.util.SharedData
import eu.kanade.tachiyomi.util.toast
import kotlinx.android.synthetic.main.activity_reader.*
import kotlinx.android.synthetic.main.reader_menu.*
@ -53,7 +55,8 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
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)
}
}
@ -81,6 +84,8 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
private var prevChapterBtn: MenuItem? = null
private val volumeKeysEnabled by lazy { preferences.readWithVolumeKeys().getOrDefault() }
val preferences: PreferencesHelper
get() = presenter.prefs
@ -88,6 +93,11 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
super.onCreate(savedState)
setContentView(R.layout.activity_reader)
if (savedState == null && SharedData.get(ReaderEvent::class.java) == null) {
finish()
return
}
setupToolbar(toolbar)
subscriptions = CompositeSubscription()
@ -174,18 +184,32 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (!isFinishing) {
val action = event.action
val keyCode = event.keyCode
when (keyCode) {
KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT -> {
if (action == KeyEvent.ACTION_UP)
viewer?.moveToNext()
return true
when (event.keyCode) {
KeyEvent.KEYCODE_VOLUME_DOWN -> {
if (volumeKeysEnabled) {
if (event.action == KeyEvent.ACTION_UP) {
viewer?.moveToNext()
}
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()
}
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 true
}
}
}
}
@ -258,7 +282,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
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
var fragment: BaseReader? = supportFragmentManager.findFragmentByTag(manga.viewer.toString()) as? BaseReader
@ -479,13 +503,15 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
private fun setCustomBrightness(enabled: Boolean) {
if (enabled) {
customBrightnessSubscription = preferences.customBrightnessValue().asObservable()
.map { Math.max(0.01f, it) }
.subscribe { setCustomBrightnessValue(it) }
subscriptions.add(customBrightnessSubscription)
} else {
if (customBrightnessSubscription != null)
if (customBrightnessSubscription != null) {
subscriptions.remove(customBrightnessSubscription)
setCustomBrightnessValue(-1f)
}
setCustomBrightnessValue(WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE)
}
}

View File

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

View File

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.reader.viewer.base
import com.davemorrissey.labs.subscaleview.decoder.*
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.ui.base.fragment.BaseFragment
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
@ -54,6 +55,11 @@ abstract class BaseReader : BaseFragment() {
lateinit var bitmapDecoderClass: Class<out ImageDecoder>
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
* restarting requests when changing pages.
@ -72,7 +78,7 @@ abstract class BaseReader : BaseFragment() {
* Returns the active 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
if (positionX < pager.width * LEFT_REGION) {
onLeftSideTap()
if (tappingEnabled) onLeftSideTap()
} else if (positionX > pager.width * RIGHT_REGION) {
onRightSideTap()
if (tappingEnabled) onRightSideTap()
} else {
readerActivity.onCenterSingleTap()
}

View File

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

View File

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

View File

@ -103,21 +103,17 @@ class RecentChaptersFragment : BaseRxFragment<RecentChaptersPresenter>(), Flexib
/**
* Open chapter in reader
*
* @param chapter selected chapter
*/
private fun openChapter(chapter: MangaChapter) {
// Start reader event
presenter.onOpenChapter(chapter)
//Start reader intent
val intent = ReaderActivity.newIntent(activity)
val intent = ReaderActivity.newIntent(activity, chapter.manga, chapter.chapter)
startActivity(intent)
}
/**
* Populate adapter with chapters
*
* @param chapters list of chapters
*/
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.source.SourceManager
import eu.kanade.tachiyomi.event.DownloadChaptersEvent
import eu.kanade.tachiyomi.event.ReaderEvent
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.SharedData
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
@ -251,14 +249,6 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
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
* @param selectedChapter chapter that is selected

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.setting
import android.os.Bundle
import android.support.v7.preference.SwitchPreferenceCompat
import android.view.View
import com.afollestad.materialdialogs.MaterialDialog
import eu.kanade.tachiyomi.BuildConfig
@ -27,6 +28,10 @@ class SettingsAboutFragment : SettingsNestedFragment() {
*/
private var releaseSubscription: Subscription? = null
val automaticUpdateToggle by lazy {
findPreference(getString(R.string.pref_enable_automatic_updates_key)) as SwitchPreferenceCompat
}
companion object {
fun newInstance(resourcePreference: Int, resourceTitle: Int): SettingsNestedFragment {
@ -45,11 +50,19 @@ class SettingsAboutFragment : SettingsNestedFragment() {
else
BuildConfig.VERSION_NAME
//Set onClickListener to check for new version
version.setOnPreferenceClickListener {
if (!BuildConfig.DEBUG && BuildConfig.INCLUDE_UPDATER)
checkVersion()
true
if (!BuildConfig.DEBUG && BuildConfig.INCLUDE_UPDATER) {
//Set onClickListener to check for new version
version.setOnPreferenceClickListener {
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()

View File

@ -14,12 +14,13 @@ import com.nononsenseapps.filepicker.FilePickerActivity
import com.nononsenseapps.filepicker.FilePickerFragment
import com.nononsenseapps.filepicker.LogicHandler
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.util.inflate
import rx.Subscription
import java.io.File
class SettingsDownloadsFragment : SettingsNestedFragment() {
val downloadDirPref by lazy { findPreference(getString(R.string.pref_download_directory_key)) }
companion object {
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?) {
downloadDirPref.setOnPreferenceClickListener {
val currentDir = preferences.downloadsDirectory().getOrDefault()
val externalDirs = getExternalFilesDirs()
val selectedIndex = externalDirs.indexOf(File(preferences.downloadsDirectory))
val selectedIndex = externalDirs.indexOf(File(currentDir))
MaterialDialog.Builder(activity)
.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_CREATE_DIR, true)
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)
} else {
// One of the predefined folders was selected
preferences.downloadsDirectory = text.toString()
updateDownloadsDir()
preferences.downloadsDirectory().set(text.toString())
}
true
})
@ -60,15 +65,14 @@ class SettingsDownloadsFragment : SettingsNestedFragment() {
true
}
downloadDirSubscription = preferences.downloadsDirectory().asObservable()
.subscribe { downloadDirPref.summary = it }
}
override fun onResume() {
super.onResume()
updateDownloadsDir()
}
fun updateDownloadsDir() {
downloadDirPref.summary = preferences.downloadsDirectory
override fun onDestroyView() {
downloadDirSubscription?.unsubscribe()
super.onDestroyView()
}
fun getExternalFilesDirs(): List<File> {
@ -81,7 +85,7 @@ class SettingsDownloadsFragment : SettingsNestedFragment() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
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.os.Bundle
import android.support.v14.preference.MultiSelectListPreference
import android.support.v4.app.TaskStackBuilder
import android.support.v7.preference.Preference
import android.view.View
@ -33,13 +34,28 @@ class SettingsGeneralFragment : SettingsNestedFragment() {
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 {
findPreference(getString(R.string.pref_theme_key)) as IntListPreference
}
var updateIntervalSubscription: Subscription? = null
var columnsSubscription: Subscription? = null
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 ->
LibraryUpdateAlarm.startAlarm(activity, (newValue as String).toInt())
true
@ -57,17 +73,11 @@ class SettingsGeneralFragment : SettingsNestedFragment() {
}
}
override fun onResume() {
super.onResume()
columnsSubscription = Observable.combineLatest(preferences.portraitColumns().asObservable(),
preferences.landscapeColumns().asObservable(),
{ portraitColumns, landscapeColumns -> Pair(portraitColumns, landscapeColumns) })
.subscribe { updateColumnsSummary(it.first, it.second) }
}
override fun onPause() {
override fun onDestroyView() {
updateIntervalSubscription?.unsubscribe()
columnsSubscription?.unsubscribe()
super.onPause()
super.onDestroyView()
}
override fun onDisplayPreferenceDialog(preference: Preference) {

View File

@ -44,9 +44,5 @@ open class SettingsNestedFragment : PreferenceFragment() {
get() = settingsActivity.preferences
val fragmentManagerCompat: FragmentManager
get() = if (Build.VERSION.SDK_INT >= 17) {
childFragmentManager
} else {
fragmentManager
}
get() = if (Build.VERSION.SDK_INT >= 17) 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.PreferenceGroup
import android.view.View
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.source.base.Source
import eu.kanade.tachiyomi.data.source.getLanguages
@ -65,7 +64,7 @@ class SettingsSourcesFragment : SettingsNestedFragment() {
fun createSource(source: Source): Preference {
return LoginPreference(preferenceManager.context).apply {
key = PreferencesHelper.SOURCE_ACCOUNT_USERNAME + source.id
key = preferences.keys.sourceUsername(source.id)
title = source.visibleName
setOnPreferenceClickListener {
@ -80,7 +79,7 @@ class SettingsSourcesFragment : SettingsNestedFragment() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
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()
}
}

View File

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

View File

@ -11,7 +11,7 @@ object DeviceUtil {
val intent = context.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
intent?.let {
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
}

View File

@ -5,10 +5,6 @@ import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import okio.BufferedSink;
import okio.BufferedSource;
import okio.Okio;
public final class DiskUtils {
private DiskUtils() {
@ -38,35 +34,7 @@ public final class DiskUtils {
}
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) {
if (inputFile.isDirectory()) {
for (File childFile : inputFile.listFiles()) {
@ -74,6 +42,7 @@ public final class DiskUtils {
}
}
//noinspection ResultOfMethodCallIgnored
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.
*/
private val map = HashMap<Class<*>, Any>()
val map = HashMap<Class<*>, Any>()
/**
* 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) }
/**
* 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.widget.TextView
import eu.kanade.tachiyomi.R
import java.util.*
class PTSansTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
@ -14,6 +15,9 @@ class PTSansTextView @JvmOverloads constructor(context: Context, attrs: Attribut
companion object {
const val PTSANS_NARROW = 0
const val PTSANS_NARROW_BOLD = 1
// Map where typefaces are cached
private val typefaces = HashMap<Int, Typeface>(2)
}
init {
@ -22,11 +26,13 @@ class PTSansTextView @JvmOverloads constructor(context: Context, attrs: Attribut
val typeface = values.getInt(R.styleable.PTSansTextView_typeface, 0)
when (typeface) {
PTSANS_NARROW -> setTypeface(Typeface.createFromAsset(context.assets, "fonts/PTSans-Narrow.ttf"))
PTSANS_NARROW_BOLD -> setTypeface(Typeface.createFromAsset(context.assets, "fonts/PTSans-NarrowBold.ttf"))
else -> throw IllegalArgumentException("Font not found " + typeface)
}
setTypeface(typefaces.getOrPut(typeface) {
Typeface.createFromAsset(context.assets, when (typeface) {
PTSANS_NARROW -> "fonts/PTSans-Narrow.ttf"
PTSANS_NARROW_BOLD -> "fonts/PTSans-NarrowBold.ttf"
else -> throw IllegalArgumentException("Font not found " + typeface)
})
})
values.recycle()
}

View File

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

View File

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

View File

@ -34,8 +34,8 @@ class SourceLoginDialog : LoginDialogPreference() {
override fun setCredentialsOnView(view: View) = with(view) {
dialog_title.text = getString(R.string.login_title, source.visibleName)
username.setText(preferences.getSourceUsername(source))
password.setText(preferences.getSourcePassword(source))
username.setText(preferences.sourceUsername(source))
password.setText(preferences.sourcePassword(source))
}
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_alignParentRight="true"
android:layout_centerVertical="true"
android:text="Edit"
android:text="@string/action_edit"
style="@style/TextAppearance.Medium.Button"/>
</RelativeLayout>

View File

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

View File

@ -126,4 +126,14 @@
<item>48</item>
</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>

View File

@ -13,10 +13,10 @@
<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_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_ask_update_manga_sync_key">pref_ask_update_manga_sync_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_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_image_decoder_key">pref_image_decoder_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_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_database_key">pref_clear_database_key</string>
<string name="pref_version">pref_version</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_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_sort">Sort</string>
<string name="action_force">Force refresh</string>
<string name="action_install">Install</string>
<!-- Operations -->
<string name="deleting">Deleting…</string>
@ -72,8 +73,6 @@
<string name="landscape">Landscape</string>
<string name="default_columns">Default</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_1hour">Hourly</string>
<string name="update_2hour">Every 2 hours</string>
@ -82,13 +81,17 @@
<string name="update_12hour">Every 12 hours</string>
<string name="update_24hour">Daily</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_ask_update_manga_sync">Confirm before updating</string>
<string name="pref_theme">Application theme</string>
<string name="light_theme">Main theme</string>
<string name="dark_theme">Dark theme</string>
<!-- Reader section -->
<string name="pref_hide_status_bar">Hide status bar</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_seamless_mode">Seamless chapter transitions</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="white_background">White</string>
<string name="black_background">Black</string>
@ -155,11 +161,16 @@
<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="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 -->
<string name="version">Version</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 -->
<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>
@ -275,6 +286,13 @@
<string name="update_check_download_started">Download started</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-->
<string name="description_backdrop">Backdrop image 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">
<SwitchPreferenceCompat
android:defaultValue="true"
android:key="acra.enable"
android:title="@string/pref_enable_acra"
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
android:key="@string/pref_version"
android:title="@string/version"
android:persistent="false" />
android:persistent="false"
android:title="@string/version"/>
<Preference
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>

View File

@ -3,12 +3,18 @@
xmlns:android="http://schemas.android.com/apk/res/android">
<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
android:title="@string/pref_clear_database"
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>

View File

@ -24,14 +24,16 @@
android:summary="%s"
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
android:defaultValue="false"
android:key="@string/pref_update_only_non_completed_key"
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>

View File

@ -2,30 +2,6 @@
<android.support.v7.preference.PreferenceScreen
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
android:title="@string/pref_viewer_type"
android:key="@string/pref_default_viewer_key"
@ -74,4 +50,49 @@
android:defaultValue="0"
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>

View File

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