mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-26 19:17:51 +02:00
Compare commits
31 Commits
Author | SHA1 | Date | |
---|---|---|---|
0cffb9e503 | |||
e2ecf0ce5f | |||
5d396bfb7c | |||
de6cc8394e | |||
4b7159648a | |||
eb9c5f95db | |||
55e9d2880c | |||
ec9c19ce7d | |||
31731e8f26 | |||
bfb12bc7c1 | |||
4befcf3819 | |||
cb58145361 | |||
b83efd90a8 | |||
9f0da3f1d6 | |||
50ae08ed8d | |||
5385642a5b | |||
0a27d4e185 | |||
bd8b9febd2 | |||
a30705f197 | |||
877032a757 | |||
19bf47b6d2 | |||
a9bfeb058b | |||
9213fc6999 | |||
447dfd1e3c | |||
638d3a32cf | |||
17c59657c3 | |||
81bce8ef76 | |||
78314077bb | |||
a7840bc247 | |||
6d0254c5e5 | |||
06681a3db7 |
@ -1,6 +1,6 @@
|
||||
| Build | Download | Auto Update |
|
||||
|-------|----------|-------------|
|
||||
| [](https://teamcity.kanade.eu/project.html?projectId=tachiyomi) [](https://travis-ci.org/inorichi/tachiyomi) | [](https://github.com/inorichi/tachiyomi/releases) [](http://tachiyomi.kanade.eu/latest/app-debug.apk) | [](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi) [](//github.com/inorichi/tachiyomi/wiki/FDroid-for-debug-versions) |
|
||||
| [](https://teamcity.kanade.eu/project.html?projectId=tachiyomi) [](https://travis-ci.org/inorichi/tachiyomi) | [](https://github.com/inorichi/tachiyomi/releases) [](http://tachiyomi.kanade.eu/latest/app-debug.apk) | [](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi) [](//github.com/inorichi/tachiyomi/wiki/FDroid-for-debug-versions) |
|
||||
|
||||
## [Report an issue](https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md)
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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" />
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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>()
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
}
|
||||
|
@ -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"
|
||||
|
||||
}
|
@ -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)
|
||||
|
||||
}
|
||||
|
@ -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)
|
@ -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>?) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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"));
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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]
|
||||
}
|
||||
}
|
||||
|
@ -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, ""))
|
||||
}
|
||||
|
@ -73,5 +73,4 @@ open class BaseActivity : AppCompatActivity() {
|
||||
snack.f()
|
||||
snack.show()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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) {
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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++
|
||||
}
|
||||
|
||||
|
@ -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) })
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -91,9 +91,7 @@ class MyAnimeListDialogFragment : DialogFragment() {
|
||||
}
|
||||
|
||||
private fun onPositiveButtonClick() {
|
||||
selectedItem?.let {
|
||||
presenter.registerManga(it)
|
||||
}
|
||||
presenter.registerManga(selectedItem)
|
||||
}
|
||||
|
||||
private fun search(query: String) {
|
||||
|
@ -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 = ""
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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> {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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] }
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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>) {
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
62
app/src/main/java/eu/kanade/tachiyomi/util/OkioExtensions.kt
Normal file
62
app/src/main/java/eu/kanade/tachiyomi/util/OkioExtensions.kt
Normal 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)
|
||||
}
|
||||
}
|
@ -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
|
||||
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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 |
@ -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>
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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();
|
||||
|
Reference in New Issue
Block a user