Compare commits

...

24 Commits

Author SHA1 Message Date
634356e72f Release v0.6.7 2018-01-09 20:42:44 +01:00
6d3cc16ab1 Include minor changes from extensions PR 2018-01-09 12:27:45 +01:00
6027671c09 Address #1154 (#1160)
* change add to library icon add toast

* adjusted toast messages
added toast to catalog long click

* adjusted strings
2018-01-08 14:08:48 +01:00
29d0cb4a15 fixed issue where some sources that use cloudflare use the Server: cloudflare as cloudflare-nginx is deprecated (#1152) 2018-01-08 11:03:37 +01:00
fe7001975a Fix padding in RecyclerViews (#1148) 2018-01-06 18:50:40 +01:00
ac88f1c146 Update README (#1142)
* Update README.md

* add pics to readme

* Update README.md

* change language from stable to just "app"

* Update README.md

* update with feedback

* change test to try

* add link to extentions repo
2018-01-05 22:54:41 +01:00
b5b86218c5 Mangachan advanced support (#1138)
* Mangachan catalogue. Add support for filtering

* MangaChan add support for status
2018-01-04 22:01:42 +01:00
bdcc6e52e6 Small new user improvements (#1143)
- Changed empty library string
- Added empty view for Categories
2018-01-01 14:57:20 +01:00
0eae817aa6 Update MangaChan.kt (#1128)
Remove useless ganres
2017-12-14 13:28:24 +01:00
8994b42760 Remove local broadcast receiver to prevent race conditions (#1123)
* Remove local broadcast receiver to prevent run exceptions.
Added option to set tile for extension update.
2017-12-11 20:01:28 +01:00
6a63ce992a [Mangafox] update mangafox URL for built-in source (#1119) 2017-12-09 13:29:30 +01:00
9ae6285eef Change discord invite link in settings (#1112)
* Change discord invite link in settings

* Change discord link is readme
2017-12-06 08:41:37 +01:00
8f9737f567 Update regexp for pages from Readmanga/Mintmanga (#1111) 2017-12-05 21:21:02 +01:00
f287d313c3 Release 0.6.6 2017-12-05 19:50:53 +01:00
e745836404 Restore tracking on backup (#1097) 2017-12-04 22:55:57 +01:00
08baf798aa Give view pager unique ids, avoiding subtle bugs 2017-12-04 22:22:35 +01:00
8bcb14c65d Add view caching to view holders 2017-12-03 17:00:32 +01:00
d94dc68830 Fix library not being updated 2017-12-03 12:59:51 +01:00
297fed6aef Repackage catalogue to match the UI 2017-12-03 12:58:38 +01:00
d690d6e0e3 Use synthetic view's new caching method 2017-12-03 01:03:15 +01:00
9ba8d88b07 Dependency updates 2017-12-02 20:59:35 +01:00
34a40b0131 Start downloader after a library update. It should help with some catalogue issues 2017-12-02 17:29:05 +01:00
182bf5f2bd Add install packages permission. Fixes #1104 2017-12-02 17:10:31 +01:00
04638535d8 Fix library options menu shown in chapters screen. Resolves #1096 2017-11-30 15:37:20 +01:00
113 changed files with 2416 additions and 2318 deletions

BIN
.github/readme-images/app-icon.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
.github/readme-images/screens.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1022 KiB

View File

@ -1,25 +1,38 @@
| Build | Download | F-Droid | Contribute | Contact |
|-------|----------|---------|------------|---------|
| [![Travis](https://img.shields.io/travis/inorichi/tachiyomi.svg)](https://travis-ci.org/inorichi/tachiyomi) | [![stable release](https://img.shields.io/github/release/inorichi/tachiyomi.svg?maxAge=3600&label=stable)](https://github.com/inorichi/tachiyomi/releases) [![latest dev build](https://img.shields.io/badge/dev-latest%20build-blue.svg)](http://tachiyomi.kanade.eu/latest) | [![fdroid release](https://img.shields.io/badge/stable-f--droid.org-blue.svg)](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi) [![fdroid dev](https://img.shields.io/badge/dev-wiki-blue.svg)](//github.com/inorichi/tachiyomi/wiki/FDroid-for-dev-versions) | [![Translation status](http://weblate.j2ghz.com/widgets/tachiyomi/-/svg-badge.svg)](https://github.com/inorichi/tachiyomi/wiki/Translation) | [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/WrBkRk4) |
| [![Travis](https://img.shields.io/travis/inorichi/tachiyomi.svg)](https://travis-ci.org/inorichi/tachiyomi) | [![stable release](https://img.shields.io/github/release/inorichi/tachiyomi.svg?maxAge=3600&label=stable)](https://github.com/inorichi/tachiyomi/releases) [![latest dev build](https://img.shields.io/badge/dev-latest%20build-blue.svg)](http://tachiyomi.kanade.eu/latest) | [![fdroid release](https://img.shields.io/badge/stable-f--droid.org-blue.svg)](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi) [![fdroid dev](https://img.shields.io/badge/dev-wiki-blue.svg)](//github.com/inorichi/tachiyomi/wiki/FDroid-for-dev-versions) | [![Translation status](http://weblate.j2ghz.com/widgets/tachiyomi/-/svg-badge.svg)](https://github.com/inorichi/tachiyomi/wiki/Translation) | [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/2dDQBv2) |
### **Contact us on [Discord](https://discord.gg/WrBkRk4)**
If you want to open an issue, please read [contributing guidelines](https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md). Your issue may be closed otherwise.
***
# ![app icon](./.github/readme-images/app-icon.png)Tachiyomi
Tachiyomi is a free and open source manga reader for Android.
Keep in mind it's still a beta, so expect it to crash sometimes.
![screenshots of app](./.github/readme-images/screens.png)
# Features
## Features
* Online and offline reading
* Configurable reader with multiple viewers and settings
* MyAnimeList support
* Resume from the next unread chapter
* Chapter filtering
* Schedule searching for updates
Features include:
* Online reading from sources like Batoto, KissManga, MangaFox, [and more](https://github.com/inorichi/tachiyomi-extensions)
* Local reading of downloaded manga
* Configurable reader with multiple viewers, reading directions and other settings
* MyAnimeList, AniList, and Kitsu support
* Categories to organize your library
* Light and dark themes
* Schedule updating your library for new chapters
* Create backups locally or to your cloud service of choice
## Download
Get the app from our [releases page](https://github.com/inorichi/tachiyomi/releases) or from [f-droid](https://f-droid.org/packages/eu.kanade.tachiyomi/).
If you want to try new features before they get to the stable release, you can download the dev version [here](http://tachiyomi.kanade.eu/latest) (auto-updates not included), or add our [F-Droid repo](https://github.com/inorichi/tachiyomi/wiki/FDroid-for-dev-versions).
## Issues, Feature Requests and Contributing
If you want to open an issue, please read [contributing guidelines](https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md). Your issue may be closed otherwise.
## FAQ
[See our wiki.](https://github.com/inorichi/tachiyomi/wiki/FAQ)
You can also reach out to us on [Discord](https://discord.gg/WrBkRk4).
## License

View File

@ -38,8 +38,8 @@ android {
minSdkVersion 16
targetSdkVersion 26
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
versionCode 28
versionName "0.6.5"
versionCode 30
versionName "0.6.7"
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
@ -106,7 +106,7 @@ dependencies {
implementation 'com.github.inorichi:junrar-android:634c1f5'
// Android support library
final support_library_version = '27.0.1'
final support_library_version = '27.0.2'
implementation "com.android.support:support-v4:$support_library_version"
implementation "com.android.support:appcompat-v7:$support_library_version"
implementation "com.android.support:cardview-v7:$support_library_version"
@ -118,7 +118,7 @@ dependencies {
implementation 'com.android.support.constraint:constraint-layout:1.0.2'
implementation 'com.android.support:multidex:1.0.1'
implementation 'com.android.support:multidex:1.0.2'
// ReactiveX
implementation 'io.reactivex:rxandroid:1.2.1'
@ -149,14 +149,14 @@ dependencies {
// Disk
implementation 'com.jakewharton:disklrucache:2.0.2'
implementation 'com.github.seven332:unifile:1.0.0'
implementation 'com.github.inorichi:unifile:e9ee588'
// HTML parser
implementation 'org.jsoup:jsoup:1.10.2'
// Job scheduling
implementation 'com.evernote:android-job:1.2.0'
implementation 'com.google.android.gms:play-services-gcm:11.6.0'
implementation 'com.evernote:android-job:1.2.1'
implementation 'com.google.android.gms:play-services-gcm:11.6.2'
// Changelog
implementation 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
@ -232,7 +232,7 @@ dependencies {
}
buildscript {
ext.kotlin_version = '1.1.61'
ext.kotlin_version = '1.2.0'
repositories {
mavenCentral()
}
@ -250,3 +250,6 @@ kotlin {
coroutines 'enable'
}
}
androidExtensions {
experimental = true
}

View File

@ -8,6 +8,7 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<application
@ -85,7 +86,7 @@
android:exported="false" />
<service
android:name=".data.updater.UpdateDownloaderService"
android:name=".data.updater.UpdaterService"
android:exported="false" />
<service

View File

@ -8,7 +8,7 @@ import com.evernote.android.job.JobManager
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob
import eu.kanade.tachiyomi.data.updater.UpdaterJob
import eu.kanade.tachiyomi.util.LocaleHelper
import org.acra.ACRA
import org.acra.annotation.ReportsCrashes
@ -28,11 +28,11 @@ open class App : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
Injekt = InjektScope(DefaultRegistrar())
Injekt.importModule(AppModule(this))
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
setupAcra()
setupJobManager()
setupNotificationChannels()
@ -60,7 +60,7 @@ open class App : Application() {
JobManager.create(this).addJobCreator { tag ->
when (tag) {
LibraryUpdateJob.TAG -> LibraryUpdateJob()
UpdateCheckerJob.TAG -> UpdateCheckerJob()
UpdaterJob.TAG -> UpdaterJob()
BackupCreatorJob.TAG -> BackupCreatorJob()
else -> null
}

View File

@ -3,7 +3,7 @@ package eu.kanade.tachiyomi
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob
import eu.kanade.tachiyomi.data.updater.UpdaterJob
import java.io.File
object Migrations {
@ -25,7 +25,7 @@ object Migrations {
if (oldVersion < 14) {
// Restore jobs after upgrading to evernote's job scheduler.
if (BuildConfig.INCLUDE_UPDATER && preferences.automaticUpdates()) {
UpdateCheckerJob.setupTask()
UpdaterJob.setupTask()
}
LibraryUpdateJob.setupTask()
}

View File

@ -23,6 +23,7 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.*
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.util.syncChaptersWithSource
@ -41,6 +42,11 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
*/
internal val sourceManager: SourceManager by injectLazy()
/**
* Tracking manager
*/
internal val trackManager: TrackManager by injectLazy()
/**
* Version of parser
*/
@ -67,18 +73,16 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
parser = initParser()
}
private fun initParser(): Gson {
return when (version) {
1 -> GsonBuilder().create()
2 -> GsonBuilder()
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
.create()
else -> throw Exception("Json version unknown")
}
private fun initParser(): Gson = when (version) {
1 -> GsonBuilder().create()
2 -> GsonBuilder()
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
.create()
else -> throw Exception("Json version unknown")
}
/**
@ -300,23 +304,26 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
val trackToUpdate = ArrayList<Track>()
for (track in tracks) {
var isInDatabase = false
for (dbTrack in dbTracks) {
if (track.sync_id == dbTrack.sync_id) {
// The sync is already in the db, only update its fields
if (track.remote_id != dbTrack.remote_id) {
dbTrack.remote_id = track.remote_id
val service = trackManager.getService(track.sync_id)
if (service != null && service.isLogged) {
var isInDatabase = false
for (dbTrack in dbTracks) {
if (track.sync_id == dbTrack.sync_id) {
// The sync is already in the db, only update its fields
if (track.remote_id != dbTrack.remote_id) {
dbTrack.remote_id = track.remote_id
}
dbTrack.last_chapter_read = Math.max(dbTrack.last_chapter_read, track.last_chapter_read)
isInDatabase = true
trackToUpdate.add(dbTrack)
break
}
dbTrack.last_chapter_read = Math.max(dbTrack.last_chapter_read, track.last_chapter_read)
isInDatabase = true
trackToUpdate.add(dbTrack)
break
}
}
if (!isInDatabase) {
// Insert new sync. Let the db assign the id
track.id = null
trackToUpdate.add(track)
if (!isInDatabase) {
// Insert new sync. Let the db assign the id
track.id = null
trackToUpdate.add(track)
}
}
}
// Update database
@ -361,32 +368,29 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
*
* @return [Manga], null if not found
*/
internal fun getMangaFromDatabase(manga: Manga): Manga? {
return databaseHelper.getManga(manga.url, manga.source).executeAsBlocking()
}
internal fun getMangaFromDatabase(manga: Manga): Manga? =
databaseHelper.getManga(manga.url, manga.source).executeAsBlocking()
/**
* Returns list containing manga from library
*
* @return [Manga] from library
*/
internal fun getFavoriteManga(): List<Manga> {
return databaseHelper.getFavoriteMangas().executeAsBlocking()
}
internal fun getFavoriteManga(): List<Manga> =
databaseHelper.getFavoriteMangas().executeAsBlocking()
/**
* Inserts manga and returns id
*
* @return id of [Manga], null if not found
*/
internal fun insertManga(manga: Manga): Long? {
return databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
}
internal fun insertManga(manga: Manga): Long? =
databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
/**
* Inserts list of chapters
*/
internal fun insertChapters(chapters: List<Chapter>) {
private fun insertChapters(chapters: List<Chapter>) {
databaseHelper.updateChaptersBackup(chapters).executeAsBlocking()
}
@ -395,7 +399,5 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
*
* @return number of backups selected by user
*/
fun numberOfBackups(): Int {
return preferences.numberOfBackups().getOrDefault()
}
fun numberOfBackups(): Int = preferences.numberOfBackups().getOrDefault()
}

View File

@ -21,6 +21,7 @@ import eu.kanade.tachiyomi.data.backup.models.Backup.VERSION
import eu.kanade.tachiyomi.data.backup.models.DHistory
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.*
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.util.chop
import eu.kanade.tachiyomi.util.isServiceRunning
@ -49,9 +50,8 @@ class BackupRestoreService : Service() {
* @param context the application context.
* @return true if the service is running, false otherwise.
*/
fun isRunning(context: Context): Boolean {
return context.isServiceRunning(BackupRestoreService::class.java)
}
private fun isRunning(context: Context): Boolean =
context.isServiceRunning(BackupRestoreService::class.java)
/**
* Starts a service to restore a backup from Json
@ -113,7 +113,13 @@ class BackupRestoreService : Service() {
*/
private val db: DatabaseHelper by injectLazy()
lateinit var executor: ExecutorService
/**
* Tracking manager
*/
internal val trackManager: TrackManager by injectLazy()
private lateinit var executor: ExecutorService
/**
* Method called when the service is created. It injects dependencies and acquire the wake lock.
@ -142,9 +148,7 @@ class BackupRestoreService : Service() {
/**
* This method needs to be implemented, but it's not used/needed.
*/
override fun onBind(intent: Intent): IBinder? {
return null
}
override fun onBind(intent: Intent): IBinder? = null
/**
* Method called when the service receives an intent.
@ -164,7 +168,7 @@ class BackupRestoreService : Service() {
subscription = Observable.using(
{ db.lowLevel().beginTransaction() },
{ getRestoreObservable(uri).doOnNext{ db.lowLevel().setTransactionSuccessful() } },
{ getRestoreObservable(uri).doOnNext { db.lowLevel().setTransactionSuccessful() } },
{ executor.execute { db.lowLevel().endTransaction() } })
.doAfterTerminate { stopSelf(startId) }
.subscribeOn(Schedulers.from(executor))
@ -294,14 +298,14 @@ class BackupRestoreService : Service() {
val source = backupManager.sourceManager.get(manga.source) ?: return null
val dbManga = backupManager.getMangaFromDatabase(manga)
if (dbManga == null) {
return if (dbManga == null) {
// Manga not in database
return mangaFetchObservable(source, manga, chapters, categories, history, tracks)
mangaFetchObservable(source, manga, chapters, categories, history, tracks)
} else { // Manga in database
// Copy information from manga already in database
backupManager.restoreMangaNoFetch(manga, dbManga)
// Fetch rest of manga information
return mangaNoFetchObservable(source, manga, chapters, categories, history, tracks)
mangaNoFetchObservable(source, manga, chapters, categories, history, tracks)
}
}
@ -327,14 +331,12 @@ class BackupRestoreService : Service() {
.map { manga }
}
.doOnNext {
// Restore categories
backupManager.restoreCategoriesForManga(it, categories)
// Restore history
backupManager.restoreHistoryForManga(history)
// Restore tracking
backupManager.restoreTrackForManga(it, tracks)
restoreExtraForManga(it, categories, history, tracks)
}
.flatMap {
trackingFetchObservable(it, tracks)
// Convert to the manga that contains new chapters.
.map { manga }
}
.doOnCompleted {
restoreProgress += 1
@ -356,14 +358,12 @@ class BackupRestoreService : Service() {
}
}
.doOnNext {
// Restore categories
backupManager.restoreCategoriesForManga(it, categories)
// Restore history
backupManager.restoreHistoryForManga(history)
// Restore tracking
backupManager.restoreTrackForManga(it, tracks)
restoreExtraForManga(it, categories, history, tracks)
}
.flatMap { manga ->
trackingFetchObservable(manga, tracks)
// Convert to the manga that contains new chapters.
.map { manga }
}
.doOnCompleted {
restoreProgress += 1
@ -371,6 +371,17 @@ class BackupRestoreService : Service() {
}
}
private fun restoreExtraForManga(manga: Manga, categories: List<String>, history: List<DHistory>, tracks: List<Track>) {
// Restore categories
backupManager.restoreCategoriesForManga(manga, categories)
// Restore history
backupManager.restoreHistoryForManga(history)
// Restore tracking
backupManager.restoreTrackForManga(manga, tracks)
}
/**
* [Observable] that fetches chapter information
*
@ -383,10 +394,33 @@ class BackupRestoreService : Service() {
// If there's any error, return empty update and continue.
.onErrorReturn {
errors.add(Date() to "${manga.title} - ${it.message}")
Pair(emptyList<Chapter>(), emptyList<Chapter>())
Pair(emptyList(), emptyList())
}
}
/**
* [Observable] that refreshes tracking information
* @param manga manga that needs updating.
* @param tracks list containing tracks from restore file.
* @return [Observable] that contains updated track item
*/
private fun trackingFetchObservable(manga: Manga, tracks: List<Track>): Observable<Track> {
return Observable.from(tracks)
.concatMap { track ->
val service = trackManager.getService(track.sync_id)
if (service != null && service.isLogged) {
service.refresh(track)
.doOnNext { db.insertTrack(it).executeAsBlocking() }
.onErrorReturn {
errors.add(Date() to "${manga.title} - ${it.message}")
track
}
} else {
errors.add(Date() to "${manga.title} - ${service?.name} not logged in")
Observable.empty()
}
}
}
/**
* Called to update dialog in [BackupConst]

View File

@ -85,9 +85,10 @@ class DownloadManager(context: Context) {
*
* @param manga the manga of the chapters.
* @param chapters the list of chapters to enqueue.
* @param autoStart whether to start the downloader after enqueing the chapters.
*/
fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
downloader.queueChapters(manga, chapters)
fun downloadChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean = true) {
downloader.queueChapters(manga, chapters, autoStart)
}
/**

View File

@ -219,8 +219,9 @@ class Downloader(private val context: Context,
*
* @param manga the manga of the chapters to download.
* @param chapters the list of chapters to download.
* @param autoStart whether to start the downloader after enqueing the chapters.
*/
fun queueChapters(manga: Manga, chapters: List<Chapter>) = launchUI {
fun queueChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean) = launchUI {
val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchUI
// Called in background thread, the operation can be slow with SAF.
@ -261,7 +262,9 @@ class Downloader(private val context: Context,
}
// Start downloader if needed
DownloadService.start(this@Downloader.context)
if (autoStart) {
DownloadService.start(this@Downloader.context)
}
}
}

View File

@ -333,7 +333,9 @@ class LibraryUpdateService(
val dbChapters = chapters.map {
mangaChapters.find { mangaChapter -> mangaChapter.url == it.url }!!
}
downloadManager.downloadChapters(manga, dbChapters)
// We don't want to start downloading while the library is updating, because websites
// may don't like it and they could ban the user.
downloadManager.downloadChapters(manga, dbChapters, false)
}
/**

View File

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.notification
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.net.Uri
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.getUriCompat
import java.io.File
@ -43,11 +44,10 @@ object NotificationHandler {
* Returns [PendingIntent] that prompts user with apk install intent
*
* @param context context
* @param file file of apk that is installed
* @param uri uri of apk that is installed
*/
fun installApkPendingActivity(context: Context, file: File): PendingIntent {
fun installApkPendingActivity(context: Context, uri: Uri): PendingIntent {
val intent = Intent(Intent.ACTION_VIEW).apply {
val uri = file.getUriCompat(context)
setDataAndType(uri, "application/vnd.android.package-archive")
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
}

View File

@ -11,8 +11,8 @@ import com.google.gson.annotations.SerializedName
* @param assets assets of latest release.
*/
class GithubRelease(@SerializedName("tag_name") val version: String,
@SerializedName("body") val changeLog: String,
@SerializedName("assets") val assets: List<Assets>) {
@SerializedName("body") val changeLog: String,
@SerializedName("assets") private val assets: List<Assets>) {
/**
* Get download link of latest release from the assets.

View File

@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.updater
import eu.kanade.tachiyomi.BuildConfig
import rx.Observable
class GithubUpdateChecker() {
class GithubUpdateChecker {
private val service: GithubService = GithubService.create()

View File

@ -3,5 +3,5 @@ package eu.kanade.tachiyomi.data.updater
sealed class GithubUpdateResult {
class NewUpdate(val release: GithubRelease): GithubUpdateResult()
class NoNewUpdate(): GithubUpdateResult()
class NoNewUpdate : GithubUpdateResult()
}

View File

@ -1,147 +0,0 @@
package eu.kanade.tachiyomi.data.updater
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.support.v4.app.NotificationCompat
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.NotificationHandler
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.notificationManager
import java.io.File
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
/**
* Local [BroadcastReceiver] that runs on UI thread
* Notification calls from [UpdateDownloaderService] should be made from here.
*/
internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceiver() {
companion object {
private const val NAME = "UpdateDownloaderReceiver"
// Called to show initial notification.
internal const val NOTIFICATION_UPDATER_INITIAL = "$ID.$NAME.UPDATER_INITIAL"
// Called to show progress notification.
internal const val NOTIFICATION_UPDATER_PROGRESS = "$ID.$NAME.UPDATER_PROGRESS"
// Called to show install notification.
internal const val NOTIFICATION_UPDATER_INSTALL = "$ID.$NAME.UPDATER_INSTALL"
// Called to show error notification
internal const val NOTIFICATION_UPDATER_ERROR = "$ID.$NAME.UPDATER_ERROR"
// Value containing action of BroadcastReceiver
internal const val EXTRA_ACTION = "$ID.$NAME.ACTION"
// Value containing progress
internal const val EXTRA_PROGRESS = "$ID.$NAME.PROGRESS"
// Value containing apk path
internal const val EXTRA_APK_PATH = "$ID.$NAME.APK_PATH"
// Value containing apk url
internal const val EXTRA_APK_URL = "$ID.$NAME.APK_URL"
}
/**
* Notification shown to user
*/
private val notification = NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON)
override fun onReceive(context: Context, intent: Intent) {
when (intent.getStringExtra(EXTRA_ACTION)) {
NOTIFICATION_UPDATER_INITIAL -> basicNotification()
NOTIFICATION_UPDATER_PROGRESS -> updateProgress(intent.getIntExtra(EXTRA_PROGRESS, 0))
NOTIFICATION_UPDATER_INSTALL -> installNotification(intent.getStringExtra(EXTRA_APK_PATH))
NOTIFICATION_UPDATER_ERROR -> errorNotification(intent.getStringExtra(EXTRA_APK_URL))
}
}
/**
* Called to show basic notification
*/
private fun basicNotification() {
// Create notification
with(notification) {
setContentTitle(context.getString(R.string.app_name))
setContentText(context.getString(R.string.update_check_notification_download_in_progress))
setSmallIcon(android.R.drawable.stat_sys_download)
setOngoing(true)
}
notification.show()
}
/**
* Called to show progress notification
*
* @param progress progress of download
*/
private fun updateProgress(progress: Int) {
with(notification) {
setProgress(100, progress, false)
setOnlyAlertOnce(true)
}
notification.show()
}
/**
* Called to show install notification
*
* @param path path of file
*/
private fun installNotification(path: String) {
// Prompt the user to install the new update.
with(notification) {
setContentText(context.getString(R.string.update_check_notification_download_complete))
setSmallIcon(android.R.drawable.stat_sys_download_done)
setOnlyAlertOnce(false)
setProgress(0, 0, false)
// Install action
setContentIntent(NotificationHandler.installApkPendingActivity(context, File(path)))
addAction(R.drawable.ic_system_update_grey_24dp_img,
context.getString(R.string.action_install),
NotificationHandler.installApkPendingActivity(context, File(path)))
// Cancel action
addAction(R.drawable.ic_clear_grey_24dp_img,
context.getString(R.string.action_cancel),
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER))
}
notification.show()
}
/**
* Called to show error notification
*
* @param url url of apk
*/
private fun errorNotification(url: String) {
// Prompt the user to retry the download.
with(notification) {
setContentText(context.getString(R.string.update_check_notification_download_error))
setSmallIcon(android.R.drawable.stat_sys_warning)
setOnlyAlertOnce(false)
setProgress(0, 0, false)
// Retry action
addAction(R.drawable.ic_refresh_grey_24dp_img,
context.getString(R.string.action_retry),
UpdateDownloaderService.downloadApkPendingService(context, url))
// Cancel action
addAction(R.drawable.ic_clear_grey_24dp_img,
context.getString(R.string.action_cancel),
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER))
}
notification.show()
}
/**
* Shows a notification from this builder.
*
* @param id the id of the notification.
*/
private fun NotificationCompat.Builder.show(id: Int = Notifications.ID_UPDATER) {
context.notificationManager.notify(id, build())
}
}

View File

@ -1,67 +1,67 @@
package eu.kanade.tachiyomi.data.updater
import android.app.PendingIntent
import android.content.Intent
import android.support.v4.app.NotificationCompat
import com.evernote.android.job.Job
import com.evernote.android.job.JobManager
import com.evernote.android.job.JobRequest
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.notificationManager
class UpdateCheckerJob : Job() {
override fun onRunJob(params: Params): Result {
return GithubUpdateChecker()
.checkForUpdate()
.map { result ->
if (result is GithubUpdateResult.NewUpdate) {
val url = result.release.downloadLink
val intent = Intent(context, UpdateDownloaderService::class.java).apply {
putExtra(UpdateDownloaderService.EXTRA_DOWNLOAD_URL, url)
}
NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON).update {
setContentTitle(context.getString(R.string.app_name))
setContentText(context.getString(R.string.update_check_notification_update_available))
setSmallIcon(android.R.drawable.stat_sys_download_done)
// Download action
addAction(android.R.drawable.stat_sys_download_done,
context.getString(R.string.action_download),
PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
}
}
Job.Result.SUCCESS
}
.onErrorReturn { Job.Result.FAILURE }
// Sadly, the task needs to be synchronous.
.toBlocking()
.single()
}
fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) {
block()
context.notificationManager.notify(Notifications.ID_UPDATER, build())
}
companion object {
const val TAG = "UpdateChecker"
fun setupTask() {
JobRequest.Builder(TAG)
.setPeriodic(24 * 60 * 60 * 1000, 60 * 60 * 1000)
.setRequiredNetworkType(JobRequest.NetworkType.CONNECTED)
.setRequirementsEnforced(true)
.setUpdateCurrent(true)
.build()
.schedule()
}
fun cancelTask() {
JobManager.instance().cancelAllForTag(TAG)
}
}
package eu.kanade.tachiyomi.data.updater
import android.app.PendingIntent
import android.content.Intent
import android.support.v4.app.NotificationCompat
import com.evernote.android.job.Job
import com.evernote.android.job.JobManager
import com.evernote.android.job.JobRequest
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.notificationManager
class UpdaterJob : Job() {
override fun onRunJob(params: Params): Result {
return GithubUpdateChecker()
.checkForUpdate()
.map { result ->
if (result is GithubUpdateResult.NewUpdate) {
val url = result.release.downloadLink
val intent = Intent(context, UpdaterService::class.java).apply {
putExtra(UpdaterService.EXTRA_DOWNLOAD_URL, url)
}
NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON).update {
setContentTitle(context.getString(R.string.app_name))
setContentText(context.getString(R.string.update_check_notification_update_available))
setSmallIcon(android.R.drawable.stat_sys_download_done)
// Download action
addAction(android.R.drawable.stat_sys_download_done,
context.getString(R.string.action_download),
PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
}
}
Job.Result.SUCCESS
}
.onErrorReturn { Job.Result.FAILURE }
// Sadly, the task needs to be synchronous.
.toBlocking()
.single()
}
fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) {
block()
context.notificationManager.notify(Notifications.ID_UPDATER, build())
}
companion object {
const val TAG = "UpdateChecker"
fun setupTask() {
JobRequest.Builder(TAG)
.setPeriodic(24 * 60 * 60 * 1000, 60 * 60 * 1000)
.setRequiredNetworkType(JobRequest.NetworkType.CONNECTED)
.setRequirementsEnforced(true)
.setUpdateCurrent(true)
.build()
.schedule()
}
fun cancelTask() {
JobManager.instance().cancelAllForTag(TAG)
}
}
}

View File

@ -0,0 +1,109 @@
package eu.kanade.tachiyomi.data.updater
import android.content.Context
import android.net.Uri
import android.support.v4.app.NotificationCompat
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.NotificationHandler
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.notificationManager
/**
* DownloadNotifier is used to show notifications when downloading and update.
*
* @param context context of application.
*/
internal class UpdaterNotifier(private val context: Context) {
/**
* Builder to manage notifications.
*/
private val notification by lazy {
NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON)
}
/**
* Call to show notification.
*
* @param id id of the notification channel.
*/
private fun NotificationCompat.Builder.show(id: Int = Notifications.ID_UPDATER) {
context.notificationManager.notify(id, build())
}
/**
* Call when apk download starts.
*
* @param title tile of notification.
*/
fun onDownloadStarted(title: String) {
with(notification) {
setContentTitle(title)
setContentText(context.getString(R.string.update_check_notification_download_in_progress))
setSmallIcon(android.R.drawable.stat_sys_download)
setOngoing(true)
}
notification.show()
}
/**
* Call when apk download progress changes.
*
* @param progress progress of download (xx%/100).
*/
fun onProgressChange(progress: Int) {
with(notification) {
setProgress(100, progress, false)
setOnlyAlertOnce(true)
}
notification.show()
}
/**
* Call when apk download is finished.
*
* @param uri path location of apk.
*/
fun onDownloadFinished(uri: Uri) {
with(notification) {
setContentText(context.getString(R.string.update_check_notification_download_complete))
setSmallIcon(android.R.drawable.stat_sys_download_done)
setOnlyAlertOnce(false)
setProgress(0, 0, false)
// Install action
setContentIntent(NotificationHandler.installApkPendingActivity(context, uri))
addAction(R.drawable.ic_system_update_grey_24dp_img,
context.getString(R.string.action_install),
NotificationHandler.installApkPendingActivity(context, uri))
// Cancel action
addAction(R.drawable.ic_clear_grey_24dp_img,
context.getString(R.string.action_cancel),
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER))
}
notification.show()
}
/**
* Call when apk download throws a error
*
* @param url web location of apk to download.
*/
fun onDownloadError(url: String) {
with(notification) {
setContentText(context.getString(R.string.update_check_notification_download_error))
setSmallIcon(android.R.drawable.stat_sys_warning)
setOnlyAlertOnce(false)
setProgress(0, 0, false)
// Retry action
addAction(R.drawable.ic_refresh_grey_24dp_img,
context.getString(R.string.action_retry),
UpdaterService.downloadApkPendingService(context, url))
// Cancel action
addAction(R.drawable.ic_clear_grey_24dp_img,
context.getString(R.string.action_cancel),
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER))
}
notification.show(Notifications.ID_UPDATER)
}
}

View File

@ -2,52 +2,37 @@ package eu.kanade.tachiyomi.data.updater
import android.app.IntentService
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.ProgressListener
import eu.kanade.tachiyomi.network.newCallWithProgress
import eu.kanade.tachiyomi.util.registerLocalReceiver
import eu.kanade.tachiyomi.util.getUriCompat
import eu.kanade.tachiyomi.util.saveTo
import eu.kanade.tachiyomi.util.sendLocalBroadcastSync
import eu.kanade.tachiyomi.util.unregisterLocalReceiver
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
import java.io.File
class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.java.name) {
class UpdaterService : IntentService(UpdaterService::class.java.name) {
/**
* Network helper
*/
private val network: NetworkHelper by injectLazy()
/**
* Local [BroadcastReceiver] that runs on UI thread
* Notifier for the updater state and progress.
*/
private val updaterNotificationReceiver = UpdateDownloaderReceiver(this)
override fun onCreate() {
super.onCreate()
// Register receiver
registerLocalReceiver(updaterNotificationReceiver, IntentFilter(INTENT_FILTER_NAME))
}
override fun onDestroy() {
// Unregister receiver
unregisterLocalReceiver(updaterNotificationReceiver)
super.onDestroy()
}
private val notifier by lazy { UpdaterNotifier(this) }
override fun onHandleIntent(intent: Intent?) {
if (intent == null) return
val title = intent.getStringExtra(EXTRA_DOWNLOAD_TITLE) ?: getString(R.string.app_name)
val url = intent.getStringExtra(EXTRA_DOWNLOAD_URL) ?: return
downloadApk(url)
downloadApk(title, url)
}
/**
@ -55,9 +40,9 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
*
* @param url url location of file
*/
fun downloadApk(url: String) {
private fun downloadApk(title: String, url: String) {
// Show notification download starting.
sendInitialBroadcast()
notifier.onDownloadStarted(title)
val progressListener = object : ProgressListener {
@ -73,7 +58,7 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
if (progress > savedProgress && currentTime - 200 > lastTick) {
savedProgress = progress
lastTick = currentTime
sendProgressBroadcast(progress)
notifier.onProgressChange(progress)
}
}
}
@ -91,80 +76,32 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
response.close()
throw Exception("Unsuccessful response")
}
sendInstallBroadcast(apkFile.absolutePath)
notifier.onDownloadFinished(apkFile.getUriCompat(this))
} catch (error: Exception) {
Timber.e(error)
sendErrorBroadcast(url)
notifier.onDownloadError(url)
}
}
/**
* Show notification download starting.
*/
private fun sendInitialBroadcast() {
val intent = Intent(INTENT_FILTER_NAME).apply {
putExtra(UpdateDownloaderReceiver.EXTRA_ACTION, UpdateDownloaderReceiver.NOTIFICATION_UPDATER_INITIAL)
}
sendLocalBroadcastSync(intent)
}
/**
* Show notification progress changed
*
* @param progress progress of download
*/
private fun sendProgressBroadcast(progress: Int) {
val intent = Intent(INTENT_FILTER_NAME).apply {
putExtra(UpdateDownloaderReceiver.EXTRA_ACTION, UpdateDownloaderReceiver.NOTIFICATION_UPDATER_PROGRESS)
putExtra(UpdateDownloaderReceiver.EXTRA_PROGRESS, progress)
}
sendLocalBroadcastSync(intent)
}
/**
* Show install notification.
*
* @param path location of file
*/
private fun sendInstallBroadcast(path: String){
val intent = Intent(INTENT_FILTER_NAME).apply {
putExtra(UpdateDownloaderReceiver.EXTRA_ACTION, UpdateDownloaderReceiver.NOTIFICATION_UPDATER_INSTALL)
putExtra(UpdateDownloaderReceiver.EXTRA_APK_PATH, path)
}
sendLocalBroadcastSync(intent)
}
/**
* Show error notification.
*
* @param url url of file
*/
private fun sendErrorBroadcast(url: String){
val intent = Intent(INTENT_FILTER_NAME).apply {
putExtra(UpdateDownloaderReceiver.EXTRA_ACTION, UpdateDownloaderReceiver.NOTIFICATION_UPDATER_ERROR)
putExtra(UpdateDownloaderReceiver.EXTRA_APK_URL, url)
}
sendLocalBroadcastSync(intent)
}
companion object {
/**
* Name of Local BroadCastReceiver.
*/
private val INTENT_FILTER_NAME = UpdateDownloaderService::class.java.name
/**
* Download url.
*/
internal const val EXTRA_DOWNLOAD_URL = "${BuildConfig.APPLICATION_ID}.UpdateDownloaderService.DOWNLOAD_URL"
internal const val EXTRA_DOWNLOAD_URL = "${BuildConfig.APPLICATION_ID}.UpdaterService.DOWNLOAD_URL"
/**
* Download title
*/
internal const val EXTRA_DOWNLOAD_TITLE = "${BuildConfig.APPLICATION_ID}.UpdaterService.DOWNLOAD_TITLE"
/**
* Downloads a new update and let the user install the new version from a notification.
* @param context the application context.
* @param url the url to the new update.
*/
fun downloadUpdate(context: Context, url: String) {
val intent = Intent(context, UpdateDownloaderService::class.java).apply {
fun downloadUpdate(context: Context, url: String, title: String = context.getString(R.string.app_name)) {
val intent = Intent(context, UpdaterService::class.java).apply {
putExtra(EXTRA_DOWNLOAD_TITLE, title)
putExtra(EXTRA_DOWNLOAD_URL, url)
}
context.startService(intent)
@ -177,7 +114,7 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
* @return [PendingIntent]
*/
internal fun downloadApkPendingService(context: Context, url: String): PendingIntent {
val intent = Intent(context, UpdateDownloaderService::class.java).apply {
val intent = Intent(context, UpdaterService::class.java).apply {
putExtra(EXTRA_DOWNLOAD_URL, url)
}
return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)

View File

@ -14,12 +14,14 @@ class CloudflareInterceptor : Interceptor {
private val challengePattern = Regex("""name="jschl_vc" value="(\w+)"""")
private val serverCheck = arrayOf("cloudflare-nginx", "cloudflare")
@Synchronized
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
// Check if Cloudflare anti-bot is on
if (response.code() == 503 && "cloudflare-nginx" == response.header("Server")) {
if (response.code() == 503 && serverCheck.contains(response.header("Server"))) {
return chain.proceed(resolveChallenge(response))
}

View File

@ -18,16 +18,6 @@ class NetworkHelper(context: Context) {
.cache(Cache(cacheDir, cacheSize))
.build()
val forceCacheClient = client.newBuilder()
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.removeHeader("Pragma")
.header("Cache-Control", "max-age=600")
.build()
}
.build()
val cloudflareClient = client.newBuilder()
.addInterceptor(CloudflareInterceptor())
.build()

View File

@ -17,7 +17,7 @@ class Mangafox : ParsedHttpSource() {
override val name = "Mangafox"
override val baseUrl = "http://mangafox.me"
override val baseUrl = "http://mangafox.la"
override val lang = "en"

View File

@ -35,13 +35,59 @@ class Mangachan : ParsedHttpSource() {
val url = if (query.isNotEmpty()) {
"$baseUrl/?do=search&subaction=search&story=$query&search_start=$pageNum"
} else {
val filt = filters.filterIsInstance<Genre>().filter { !it.isIgnored() }
if (filt.isNotEmpty()) {
var genres = ""
filt.forEach { genres += (if (it.isExcluded()) "-" else "") + it.id + '+' }
"$baseUrl/tags/${genres.dropLast(1)}?offset=${20 * (pageNum - 1)}"
var genres = ""
var order = ""
var statusParam = true
var status = ""
for (filter in if (filters.isEmpty()) getFilterList() else filters) {
when (filter) {
is GenreList -> {
filter.state.forEach { f ->
if (!f.isIgnored()) {
genres += (if (f.isExcluded()) "-" else "") + f.id + '+'
}
}
}
is OrderBy -> { if (filter.state!!.ascending && filter.state!!.index == 0) { statusParam = false } }
is Status -> status = arrayOf("", "all_done", "end", "ongoing", "new_ch")[filter.state]
}
}
if (genres.isNotEmpty()) {
for (filter in filters) {
when (filter) {
is OrderBy -> {
order = if (filter.state!!.ascending) {
arrayOf("", "&n=favasc", "&n=abcdesc", "&n=chasc")[filter.state!!.index]
} else {
arrayOf("&n=dateasc", "&n=favdesc", "&n=abcasc", "&n=chdesc")[filter.state!!.index]
}
}
}
}
if (statusParam) {
"$baseUrl/tags/${genres.dropLast(1)}$order?offset=${20 * (pageNum - 1)}&status=$status"
} else {
"$baseUrl/tags/$status/${genres.dropLast(1)}/$order?offset=${20 * (pageNum - 1)}"
}
} else {
"$baseUrl/?do=search&subaction=search&story=$query&search_start=$pageNum"
for (filter in filters) {
when (filter) {
is OrderBy -> {
order = if (filter.state!!.ascending) {
arrayOf("manga/new", "manga/new&n=favasc", "manga/new&n=abcdesc", "manga/new&n=chasc")[filter.state!!.index]
} else {
arrayOf("manga/new&n=dateasc", "mostfavorites", "catalog", "sortch")[filter.state!!.index]
}
}
}
}
if (statusParam) {
"$baseUrl/$order?offset=${20 * (pageNum - 1)}&status=$status"
} else {
"$baseUrl/$order/$status?offset=${20 * (pageNum - 1)}"
}
}
}
return GET(url, headers)
@ -160,18 +206,39 @@ class Mangachan : ParsedHttpSource() {
override fun imageUrlParse(document: Document) = ""
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Тэги", genres)
private class Genre(name: String, val id: String = name.replace(' ', '_')) : Filter.TriState(name)
private class Status : Filter.Select<String>("Статус", arrayOf("Все", "Перевод завершен", "Выпуск завершен", "Онгоинг", "Новые главы"))
private class OrderBy : Filter.Sort("Сортировка",
arrayOf("Дата", "Популярность", "Имя", "Главы"),
Filter.Sort.Selection(1, false))
override fun getFilterList() = FilterList(
Status(),
OrderBy(),
GenreList(getGenreList())
)
// private class StatusList(status: List<Status>) : Filter.Group<Status>("Статус", status)
// private class Status(name: String, val id: String) : Filter.CheckBox(name, false)
// private fun getStatusList() = listOf(
// Status("Перевод завершен", "/all_done"),
// Status("Выпуск завершен", "/end"),
// Status("Онгоинг", "/ongoing"),
// Status("Новые главы", "/new_ch")
// )
/* [...document.querySelectorAll("li.sidetag > a:nth-child(1)")].map((el,i) =>
* { const link=el.getAttribute('href');const id=link.substr(6,link.length);
* return `Genre("${id.replace("_", " ")}")` }).join(',\n')
* on http://mangachan.me/
*/
override fun getFilterList() = FilterList(
private fun getGenreList() = listOf(
Genre("18 плюс"),
Genre("bdsm"),
Genre("арт"),
Genre("биография"),
Genre("боевик"),
Genre("боевые искусства"),
Genre("вампиры"),
@ -191,7 +258,6 @@ class Mangachan : ParsedHttpSource() {
Genre("кодомо"),
Genre("комедия"),
Genre("литРПГ"),
Genre("магия"),
Genre("махо-сёдзё"),
Genre("меха"),
Genre("мистика"),
@ -213,6 +279,7 @@ class Mangachan : ParsedHttpSource() {
Genre("сёдзё-ай"),
Genre("сёнэн"),
Genre("сёнэн-ай"),
Genre("темное фэнтези"),
Genre("тентакли"),
Genre("трагедия"),
Genre("триллер"),
@ -226,4 +293,4 @@ class Mangachan : ParsedHttpSource() {
Genre("яой"),
Genre("ёнкома")
)
}
}

View File

@ -118,7 +118,7 @@ class Mintmanga : ParsedHttpSource() {
val endIndex = html.indexOf("], 0, false);", beginIndex)
val trimmedHtml = html.substring(beginIndex, endIndex)
val p = Pattern.compile("'.+?','.+?',\".+?\"")
val p = Pattern.compile("'.*?','.*?',\".*?\"")
val m = p.matcher(trimmedHtml)
val pages = mutableListOf<Page>()

View File

@ -118,7 +118,7 @@ class Readmanga : ParsedHttpSource() {
val endIndex = html.indexOf("], 0, false);", beginIndex)
val trimmedHtml = html.substring(beginIndex, endIndex)
val p = Pattern.compile("'.+?','.+?',\".+?\"")
val p = Pattern.compile("'.*?','.*?',\".*?\"")
val m = p.matcher(trimmedHtml)
val pages = mutableListOf<Page>()

View File

@ -6,21 +6,39 @@ import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import com.bluelinelabs.conductor.RestoreViewOnCreateController
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.*
abstract class BaseController(bundle: Bundle? = null) : RestoreViewOnCreateController(bundle) {
abstract class BaseController(bundle: Bundle? = null) : RestoreViewOnCreateController(bundle),
LayoutContainer {
init {
addLifecycleListener(object : LifecycleListener() {
override fun postCreateView(controller: Controller, view: View) {
onViewCreated(view)
}
})
}
override val containerView: View?
get() = view
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View {
val view = inflateView(inflater, container)
onViewCreated(view, savedViewState)
return view
return inflateView(inflater, container)
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
clearFindViewByIdCache()
}
abstract fun inflateView(inflater: LayoutInflater, container: ViewGroup): View
open fun onViewCreated(view: View, savedViewState: Bundle?) { }
open fun onViewCreated(view: View) { }
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
if (type.isEnter) {

View File

@ -5,6 +5,8 @@ import android.os.Build
import android.support.v4.content.ContextCompat
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.Router
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
fun Router.popControllerWithTag(tag: String): Boolean {
val controller = getControllerWithTag(tag)
@ -24,4 +26,10 @@ fun Controller.requestPermissionsSafe(permissions: Array<String>, requestCode: I
}
}
}
}
}
fun Controller.withFadeTransaction(): RouterTransaction {
return RouterTransaction.with(this)
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler())
}

View File

@ -30,7 +30,7 @@ abstract class RxController(bundle: Bundle? = null) : BaseController(bundle) {
}
@CallSuper
override fun onViewCreated(view: View, savedViewState: Bundle?) {
override fun onViewCreated(view: View) {
if (untilDestroySubscriptions.isUnsubscribed) {
untilDestroySubscriptions = CompositeSubscription()
}

View File

@ -0,0 +1,15 @@
package eu.kanade.tachiyomi.ui.base.holder
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.viewholders.FlexibleViewHolder
import kotlinx.android.extensions.LayoutContainer
abstract class BaseFlexibleViewHolder(view: View,
adapter: FlexibleAdapter<*>,
stickyHeader: Boolean = false) :
FlexibleViewHolder(view, adapter, stickyHeader), LayoutContainer {
override val containerView: View?
get() = itemView
}

View File

@ -0,0 +1,11 @@
package eu.kanade.tachiyomi.ui.base.holder
import android.support.v7.widget.RecyclerView
import android.view.View
import kotlinx.android.extensions.LayoutContainer
abstract class BaseViewHolder(view: View) : RecyclerView.ViewHolder(view), LayoutContainer {
override val containerView: View?
get() = itemView
}

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.base.presenter
import nucleus.presenter.RxPresenter
import nucleus.presenter.delivery.Delivery
import rx.Observable
open class BasePresenter<V> : RxPresenter<V>() {
@ -35,4 +36,29 @@ open class BasePresenter<V> : RxPresenter<V>() {
fun <T> Observable<T>.subscribeReplay(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit)? = null)
= compose(deliverReplay<T>()).subscribe(split(onNext, onError)).apply { add(this) }
/**
* Subscribes an observable with [DeliverWithView] and adds it to the presenter's lifecycle
* subscription list.
*
* @param onNext function to execute when the observable emits an item.
* @param onError function to execute when the observable throws an error.
*/
fun <T> Observable<T>.subscribeWithView(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit)? = null)
= compose(DeliverWithView<V, T>(view())).subscribe(split(onNext, onError)).apply { add(this) }
/**
* A deliverable that only emits to the view if attached, otherwise the event is ignored.
*/
class DeliverWithView<View, T>(private val view: Observable<View>) : Observable.Transformer<T, Delivery<View, T>> {
override fun call(observable: Observable<T>): Observable<Delivery<View, T>> {
return observable
.materialize()
.filter { notification -> !notification.isOnCompleted }
.flatMap { notification ->
view.take(1).filter { it != null }.map { Delivery(it, notification) }
}
}
}
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.catalogue.main
package eu.kanade.tachiyomi.ui.catalogue
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
@ -8,9 +8,9 @@ import eu.kanade.tachiyomi.util.getResourceColor
/**
* Adapter that holds the catalogue cards.
*
* @param controller instance of [CatalogueMainController].
* @param controller instance of [CatalogueController].
*/
class CatalogueMainAdapter(val controller: CatalogueMainController) :
class CatalogueAdapter(val controller: CatalogueController) :
FlexibleAdapter<IFlexible<*>>(null, controller, true) {
val cardBackground = controller.activity!!.getResourceColor(R.attr.background_card)
@ -31,7 +31,7 @@ class CatalogueMainAdapter(val controller: CatalogueMainController) :
/**
* Listener which should be called when user clicks browse.
* Note: Should only be handled by [CatalogueMainController]
* Note: Should only be handled by [CatalogueController]
*/
interface OnBrowseClickListener {
fun onBrowseClick(position: Int)
@ -39,7 +39,7 @@ class CatalogueMainAdapter(val controller: CatalogueMainController) :
/**
* Listener which should be called when user clicks latest.
* Note: Should only be handled by [CatalogueMainController]
* Note: Should only be handled by [CatalogueController]
*/
interface OnLatestClickListener {
fun onLatestClick(position: Int)

View File

@ -1,523 +1,231 @@
package eu.kanade.tachiyomi.ui.catalogue
import android.content.res.Configuration
import android.os.Bundle
import android.support.design.widget.Snackbar
import android.support.v4.widget.DrawerLayout
import android.support.v7.widget.*
import android.view.*
import com.afollestad.materialdialogs.MaterialDialog
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import com.f2prateek.rx.preferences.Preference
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.*
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import eu.kanade.tachiyomi.widget.DrawerSwipeCloseListener
import kotlinx.android.synthetic.main.catalogue_controller.view.*
import kotlinx.android.synthetic.main.main_activity.*
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.subscriptions.Subscriptions
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
import java.util.concurrent.TimeUnit
/**
* Controller to manage the catalogues available in the app.
*/
open class CatalogueController(bundle: Bundle) :
NucleusController<CataloguePresenter>(bundle),
SecondaryDrawerController,
FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener,
FlexibleAdapter.EndlessScrollListener,
ChangeMangaCategoriesDialog.Listener {
constructor(source: CatalogueSource) : this(Bundle().apply {
putLong(SOURCE_ID_KEY, source.id)
})
/**
* Preferences helper.
*/
private val preferences: PreferencesHelper by injectLazy()
/**
* Adapter containing the list of manga from the catalogue.
*/
private var adapter: FlexibleAdapter<IFlexible<*>>? = null
/**
* Snackbar containing an error message when a request fails.
*/
private var snack: Snackbar? = null
/**
* Navigation view containing filter items.
*/
private var navView: CatalogueNavigationView? = null
/**
* Recycler view with the list of results.
*/
private var recycler: RecyclerView? = null
/**
* Drawer listener to allow swipe only for closing the drawer.
*/
private var drawerListener: DrawerLayout.DrawerListener? = null
/**
* Subscription for the search view.
*/
private var searchViewSubscription: Subscription? = null
/**
* Subscription for the number of manga per row.
*/
private var numColumnsSubscription: Subscription? = null
/**
* Endless loading item.
*/
private var progressItem: ProgressItem? = null
init {
setHasOptionsMenu(true)
}
override fun getTitle(): String? {
return presenter.source.name
}
override fun createPresenter(): CataloguePresenter {
return CataloguePresenter(args.getLong(SOURCE_ID_KEY))
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
return inflater.inflate(R.layout.catalogue_controller, container, false)
}
override fun onViewCreated(view: View, savedViewState: Bundle?) {
super.onViewCreated(view, savedViewState)
// Initialize adapter, scroll listener and recycler views
adapter = FlexibleAdapter(null, this)
setupRecycler(view)
navView?.setFilters(presenter.filterItems)
view.progress?.visible()
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
numColumnsSubscription?.unsubscribe()
numColumnsSubscription = null
searchViewSubscription?.unsubscribe()
searchViewSubscription = null
adapter = null
snack = null
recycler = null
}
override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup? {
// Inflate and prepare drawer
val navView = drawer.inflate(R.layout.catalogue_drawer) as CatalogueNavigationView
this.navView = navView
drawerListener = DrawerSwipeCloseListener(drawer, navView).also {
drawer.addDrawerListener(it)
}
navView.setFilters(presenter.filterItems)
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.END)
navView.onSearchClicked = {
val allDefault = presenter.sourceFilters == presenter.source.getFilterList()
showProgressBar()
adapter?.clear()
presenter.setSourceFilter(if (allDefault) FilterList() else presenter.sourceFilters)
}
navView.onResetClicked = {
presenter.appliedFilters = FilterList()
val newFilters = presenter.source.getFilterList()
presenter.sourceFilters = newFilters
navView.setFilters(presenter.filterItems)
}
return navView
}
override fun cleanupSecondaryDrawer(drawer: DrawerLayout) {
drawerListener?.let { drawer.removeDrawerListener(it) }
drawerListener = null
navView = null
}
private fun setupRecycler(view: View) {
numColumnsSubscription?.unsubscribe()
var oldPosition = RecyclerView.NO_POSITION
val oldRecycler = view.catalogue_view?.getChildAt(1)
if (oldRecycler is RecyclerView) {
oldPosition = (oldRecycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
oldRecycler.adapter = null
view.catalogue_view?.removeView(oldRecycler)
}
val recycler = if (presenter.isListMode) {
RecyclerView(view.context).apply {
id = R.id.recycler
layoutManager = LinearLayoutManager(context)
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
}
} else {
(view.catalogue_view.inflate(R.layout.catalogue_recycler_autofit) as AutofitRecyclerView).apply {
numColumnsSubscription = getColumnsPreferenceForCurrentOrientation().asObservable()
.doOnNext { spanCount = it }
.skip(1)
// Set again the adapter to recalculate the covers height
.subscribe { adapter = this@CatalogueController.adapter }
(layoutManager as GridLayoutManager).spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return when (adapter?.getItemViewType(position)) {
R.layout.catalogue_grid_item, null -> 1
else -> spanCount
}
}
}
}
}
recycler.setHasFixedSize(true)
recycler.adapter = adapter
view.catalogue_view.addView(recycler, 1)
if (oldPosition != RecyclerView.NO_POSITION) {
recycler.layoutManager.scrollToPosition(oldPosition)
}
this.recycler = recycler
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.catalogue_list, menu)
// Initialize search menu
menu.findItem(R.id.action_search).apply {
val searchView = actionView as SearchView
val query = presenter.query
if (!query.isBlank()) {
expandActionView()
searchView.setQuery(query, true)
searchView.clearFocus()
}
val searchEventsObservable = searchView.queryTextChangeEvents()
.skip(1)
.share()
val writingObservable = searchEventsObservable
.filter { !it.isSubmitted }
.debounce(1250, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
val submitObservable = searchEventsObservable
.filter { it.isSubmitted }
searchViewSubscription?.unsubscribe()
searchViewSubscription = Observable.merge(writingObservable, submitObservable)
.map { it.queryText().toString() }
.distinctUntilChanged()
.subscribeUntilDestroy { searchWithQuery(it) }
untilDestroySubscriptions.add(
Subscriptions.create { if (isActionViewExpanded) collapseActionView() })
}
// Setup filters button
menu.findItem(R.id.action_set_filter).apply {
icon.mutate()
if (presenter.sourceFilters.isEmpty()) {
isEnabled = false
icon.alpha = 128
} else {
isEnabled = true
icon.alpha = 255
}
}
// Show next display mode
menu.findItem(R.id.action_display_mode).apply {
val icon = if (presenter.isListMode)
R.drawable.ic_view_module_white_24dp
else
R.drawable.ic_view_list_white_24dp
setIcon(icon)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_display_mode -> swapDisplayMode()
R.id.action_set_filter -> navView?.let { activity?.drawer?.openDrawer(Gravity.END) }
else -> return super.onOptionsItemSelected(item)
}
return true
}
/**
* Restarts the request with a new query.
*
* @param newQuery the new query.
*/
private fun searchWithQuery(newQuery: String) {
// If text didn't change, do nothing
if (presenter.query == newQuery)
return
// FIXME dirty fix to restore the toolbar buttons after closing search mode.
if (newQuery == "") {
activity?.invalidateOptionsMenu()
}
showProgressBar()
adapter?.clear()
presenter.restartPager(newQuery)
}
/**
* Called from the presenter when the network request is received.
*
* @param page the current page.
* @param mangas the list of manga of the page.
*/
fun onAddPage(page: Int, mangas: List<CatalogueItem>) {
val adapter = adapter ?: return
hideProgressBar()
if (page == 1) {
adapter.clear()
resetProgressItem()
}
adapter.onLoadMoreComplete(mangas)
}
/**
* Called from the presenter when the network request fails.
*
* @param error the error received.
*/
fun onAddPageError(error: Throwable) {
Timber.e(error)
val adapter = adapter ?: return
adapter.onLoadMoreComplete(null)
hideProgressBar()
val message = if (error is NoResultsException) "No results found" else (error.message ?: "")
snack?.dismiss()
snack = view?.catalogue_view?.snack(message, Snackbar.LENGTH_INDEFINITE) {
setAction(R.string.action_retry) {
// If not the first page, show bottom progress bar.
if (adapter.mainItemCount > 0) {
val item = progressItem ?: return@setAction
adapter.addScrollableFooterWithDelay(item, 0, true)
} else {
showProgressBar()
}
presenter.requestNext()
}
}
}
/**
* Sets a new progress item and reenables the scroll listener.
*/
private fun resetProgressItem() {
progressItem = ProgressItem()
adapter?.endlessTargetCount = 0
adapter?.setEndlessScrollListener(this, progressItem!!)
}
/**
* Called by the adapter when scrolled near the bottom.
*/
override fun onLoadMore(lastPosition: Int, currentPage: Int) {
Timber.e("onLoadMore")
if (presenter.hasNextPage()) {
presenter.requestNext()
} else {
adapter?.onLoadMoreComplete(null)
adapter?.endlessTargetCount = 1
}
}
override fun noMoreLoad(newItemsSize: Int) {
}
/**
* Called from the presenter when a manga is initialized.
*
* @param manga the manga initialized
*/
fun onMangaInitialized(manga: Manga) {
getHolder(manga)?.setImage(manga)
}
/**
* Swaps the current display mode.
*/
fun swapDisplayMode() {
val view = view ?: return
val adapter = adapter ?: return
presenter.swapDisplayMode()
val isListMode = presenter.isListMode
activity?.invalidateOptionsMenu()
setupRecycler(view)
if (!isListMode || !view.context.connectivityManager.isActiveNetworkMetered) {
// Initialize mangas if going to grid view or if over wifi when going to list view
val mangas = (0..adapter.itemCount-1).mapNotNull {
(adapter.getItem(it) as? CatalogueItem)?.manga
}
presenter.initializeMangas(mangas)
}
}
/**
* 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)
preferences.portraitColumns()
else
preferences.landscapeColumns()
}
/**
* Returns the view holder for the given manga.
*
* @param manga the manga to find.
* @return the holder of the manga or null if it's not bound.
*/
private fun getHolder(manga: Manga): CatalogueHolder? {
val adapter = adapter ?: return null
adapter.allBoundViewHolders.forEach { holder ->
val item = adapter.getItem(holder.adapterPosition) as? CatalogueItem
if (item != null && item.manga.id!! == manga.id!!) {
return holder as CatalogueHolder
}
}
return null
}
/**
* Shows the progress bar.
*/
private fun showProgressBar() {
view?.progress?.visible()
snack?.dismiss()
snack = null
}
/**
* Hides active progress bars.
*/
private fun hideProgressBar() {
view?.progress?.gone()
}
/**
* Called when a manga is clicked.
*
* @param position the position of the element clicked.
* @return true if the item should be selected, false otherwise.
*/
override fun onItemClick(position: Int): Boolean {
val item = adapter?.getItem(position) as? CatalogueItem ?: return false
router.pushController(RouterTransaction.with(MangaController(item.manga, true))
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler()))
return false
}
/**
* Called when a manga is long clicked.
*
* Adds the manga to the default category if none is set it shows a list of categories for the user to put the manga
* in, the list consists of the default category plus the user's categories. The default category is preselected on
* new manga, and on already favorited manga the manga's categories are preselected.
*
* @param position the position of the element clicked.
*/
override fun onItemLongClick(position: Int) {
val manga = (adapter?.getItem(position) as? CatalogueItem?)?.manga ?: return
if (manga.favorite) {
MaterialDialog.Builder(activity!!)
.items(resources?.getString(R.string.remove_from_library))
.itemsCallback { _, _, which, _ ->
when (which) {
0 -> {
presenter.changeMangaFavorite(manga)
adapter?.notifyItemChanged(position)
}
}
}.show()
} else {
presenter.changeMangaFavorite(manga)
adapter?.notifyItemChanged(position)
val categories = presenter.getCategories()
val defaultCategory = categories.find { it.id == preferences.defaultCategory() }
if (defaultCategory != null) {
presenter.moveMangaToCategory(manga, defaultCategory)
} else if (categories.size <= 1) { // default or the one from the user
presenter.moveMangaToCategory(manga, categories.firstOrNull())
} else {
val ids = presenter.getMangaCategoryIds(manga)
val preselected = ids.mapNotNull { id ->
categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
}.toTypedArray()
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
.showDialog(router)
}
}
}
/**
* Update manga to use selected categories.
*
* @param mangas The list of manga to move to categories.
* @param categories The list of categories where manga will be placed.
*/
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
val manga = mangas.firstOrNull() ?: return
presenter.updateMangaCategories(manga, categories)
}
protected companion object {
const val SOURCE_ID_KEY = "sourceId"
}
}
package eu.kanade.tachiyomi.ui.catalogue
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.SearchView
import android.view.*
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.online.LoginSource
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
import eu.kanade.tachiyomi.ui.catalogue.latest.LatestUpdatesController
import eu.kanade.tachiyomi.ui.setting.SettingsSourcesController
import eu.kanade.tachiyomi.widget.preference.SourceLoginDialog
import kotlinx.android.synthetic.main.catalogue_main_controller.*
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* This controller shows and manages the different catalogues enabled by the user.
* This controller should only handle UI actions, IO actions should be done by [CataloguePresenter]
* [SourceLoginDialog.Listener] refreshes the adapter on successful login of catalogues.
* [CatalogueAdapter.OnBrowseClickListener] call function data on browse item click.
* [CatalogueAdapter.OnLatestClickListener] call function data on latest item click
*/
class CatalogueController : NucleusController<CataloguePresenter>(),
SourceLoginDialog.Listener,
FlexibleAdapter.OnItemClickListener,
CatalogueAdapter.OnBrowseClickListener,
CatalogueAdapter.OnLatestClickListener {
/**
* Application preferences.
*/
private val preferences: PreferencesHelper = Injekt.get()
/**
* Adapter containing sources.
*/
private var adapter: CatalogueAdapter? = null
/**
* Called when controller is initialized.
*/
init {
// Enable the option menu
setHasOptionsMenu(true)
}
/**
* Set the title of controller.
*
* @return title.
*/
override fun getTitle(): String? {
return applicationContext?.getString(R.string.label_catalogues)
}
/**
* Create the [CataloguePresenter] used in controller.
*
* @return instance of [CataloguePresenter]
*/
override fun createPresenter(): CataloguePresenter {
return CataloguePresenter()
}
/**
* Initiate the view with [R.layout.catalogue_main_controller].
*
* @param inflater used to load the layout xml.
* @param container containing parent views.
* @return inflated view.
*/
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
return inflater.inflate(R.layout.catalogue_main_controller, container, false)
}
/**
* Called when the view is created
*
* @param view view of controller
*/
override fun onViewCreated(view: View) {
super.onViewCreated(view)
adapter = CatalogueAdapter(this)
// Create recycler and set adapter.
recycler.layoutManager = LinearLayoutManager(view.context)
recycler.adapter = adapter
recycler.addItemDecoration(SourceDividerItemDecoration(view.context))
}
override fun onDestroyView(view: View) {
adapter = null
super.onDestroyView(view)
}
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
super.onChangeStarted(handler, type)
if (!type.isPush && handler is SettingsSourcesFadeChangeHandler) {
presenter.updateSources()
}
}
/**
* Called when login dialog is closed, refreshes the adapter.
*
* @param source clicked item containing source information.
*/
override fun loginDialogClosed(source: LoginSource) {
if (source.isLogged()) {
adapter?.clear()
presenter.loadSources()
}
}
/**
* Called when item is clicked
*/
override fun onItemClick(position: Int): Boolean {
val item = adapter?.getItem(position) as? SourceItem ?: return false
val source = item.source
if (source is LoginSource && !source.isLogged()) {
val dialog = SourceLoginDialog(source)
dialog.targetController = this
dialog.showDialog(router)
} else {
// Open the catalogue view.
openCatalogue(source, BrowseCatalogueController(source))
}
return false
}
/**
* Called when browse is clicked in [CatalogueAdapter]
*/
override fun onBrowseClick(position: Int) {
onItemClick(position)
}
/**
* Called when latest is clicked in [CatalogueAdapter]
*/
override fun onLatestClick(position: Int) {
val item = adapter?.getItem(position) as? SourceItem ?: return
openCatalogue(item.source, LatestUpdatesController(item.source))
}
/**
* Opens a catalogue with the given controller.
*/
private fun openCatalogue(source: CatalogueSource, controller: BrowseCatalogueController) {
preferences.lastUsedCatalogueSource().set(source.id)
router.pushController(controller.withFadeTransaction())
}
/**
* Adds items to the options menu.
*
* @param menu menu containing options.
* @param inflater used to load the menu xml.
*/
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
// Inflate menu
inflater.inflate(R.menu.catalogue_main, menu)
// Initialize search option.
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
// Change hint to show global search.
searchView.queryHint = applicationContext?.getString(R.string.action_global_search_hint)
// Create query listener which opens the global search view.
searchView.queryTextChangeEvents()
.filter { it.isSubmitted }
.subscribeUntilDestroy {
val query = it.queryText().toString()
router.pushController(CatalogueSearchController(query).withFadeTransaction())
}
}
/**
* Called when an option menu item has been selected by the user.
*
* @param item The selected item.
* @return True if this event has been consumed, false if it has not.
*/
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
// Initialize option to open catalogue settings.
R.id.action_settings -> {
router.pushController((RouterTransaction.with(SettingsSourcesController()))
.popChangeHandler(SettingsSourcesFadeChangeHandler())
.pushChangeHandler(FadeChangeHandler()))
}
else -> return super.onOptionsItemSelected(item)
}
return true
}
/**
* Called to update adapter containing sources.
*/
fun setSources(sources: List<IFlexible<*>>) {
adapter?.updateDataSet(sources)
}
/**
* Called to set the last used catalogue at the top of the view.
*/
fun setLastUsedSource(item: SourceItem?) {
adapter?.removeAllScrollableHeaders()
if (item != null) {
adapter?.addScrollableHeader(item)
}
}
class SettingsSourcesFadeChangeHandler : FadeChangeHandler()
}

View File

@ -1,376 +1,104 @@
package eu.kanade.tachiyomi.ui.catalogue
import android.os.Bundle
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.flexibleadapter.items.ISectionable
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.catalogue.filter.*
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import rx.subjects.PublishSubject
import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.*
import java.util.concurrent.TimeUnit
/**
* Presenter of [CatalogueController].
* Presenter of [CatalogueController]
* Function calls should be done from here. UI calls should be done from the controller.
*
* @param sourceManager manages the different sources.
* @param preferences application preferences.
*/
open class CataloguePresenter(
sourceId: Long,
sourceManager: SourceManager = Injekt.get(),
private val db: DatabaseHelper = Injekt.get(),
private val prefs: PreferencesHelper = Injekt.get(),
private val coverCache: CoverCache = Injekt.get()
class CataloguePresenter(
val sourceManager: SourceManager = Injekt.get(),
private val preferences: PreferencesHelper = Injekt.get()
) : BasePresenter<CatalogueController>() {
/**
* Selected source.
* Enabled sources.
*/
val source = sourceManager.get(sourceId) as CatalogueSource
var sources = getEnabledSources()
/**
* Query from the view.
* Subscription for retrieving enabled sources.
*/
var query = ""
private set
/**
* Modifiable list of filters.
*/
var sourceFilters = FilterList()
set(value) {
field = value
filterItems = value.toItems()
}
var filterItems: List<IFlexible<*>> = emptyList()
/**
* List of filters used by the [Pager]. If empty alongside [query], the popular query is used.
*/
var appliedFilters = FilterList()
/**
* Pager containing a list of manga results.
*/
private lateinit var pager: Pager
/**
* Subject that initializes a list of manga.
*/
private val mangaDetailSubject = PublishSubject.create<List<Manga>>()
/**
* Whether the view is in list mode or not.
*/
var isListMode: Boolean = false
private set
/**
* Subscription for the pager.
*/
private var pagerSubscription: Subscription? = null
/**
* Subscription for one request from the pager.
*/
private var pageSubscription: Subscription? = null
/**
* Subscription to initialize manga details.
*/
private var initializerSubscription: Subscription? = null
private var sourceSubscription: Subscription? = null
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
sourceFilters = source.getFilterList()
if (savedState != null) {
query = savedState.getString(CataloguePresenter::query.name, "")
}
add(prefs.catalogueAsList().asObservable()
.subscribe { setDisplayMode(it) })
restartPager()
}
override fun onSave(state: Bundle) {
state.putString(CataloguePresenter::query.name, query)
super.onSave(state)
// Load enabled and last used sources
loadSources()
loadLastUsedSource()
}
/**
* Restarts the pager for the active source with the provided query and filters.
*
* @param query the query.
* @param filters the current state of the filters (for search mode).
* Unsubscribe and create a new subscription to fetch enabled sources.
*/
fun restartPager(query: String = this.query, filters: FilterList = this.appliedFilters) {
this.query = query
this.appliedFilters = filters
fun loadSources() {
sourceSubscription?.unsubscribe()
subscribeToMangaInitializer()
// Create a new pager.
pager = createPager(query, filters)
val sourceId = source.id
val catalogueAsList = prefs.catalogueAsList()
// Prepare the pager.
pagerSubscription?.let { remove(it) }
pagerSubscription = pager.results()
.observeOn(Schedulers.io())
.map { it.first to it.second.map { networkToLocalManga(it, sourceId) } }
.doOnNext { initializeMangas(it.second) }
.map { it.first to it.second.map { CatalogueItem(it, catalogueAsList) } }
.observeOn(AndroidSchedulers.mainThread())
.subscribeReplay({ view, (page, mangas) ->
view.onAddPage(page, mangas)
}, { _, error ->
Timber.e(error)
})
// Request first page.
requestNext()
}
/**
* Requests the next page for the active pager.
*/
fun requestNext() {
if (!hasNextPage()) return
pageSubscription?.let { remove(it) }
pageSubscription = Observable.defer { pager.requestNext() }
.subscribeFirst({ _, _ ->
// Nothing to do when onNext is emitted.
}, CatalogueController::onAddPageError)
}
/**
* Returns true if the last fetched page has a next page.
*/
fun hasNextPage(): Boolean {
return pager.hasNextPage
}
/**
* Sets the display mode.
*
* @param asList whether the current mode is in list or not.
*/
private fun setDisplayMode(asList: Boolean) {
isListMode = asList
subscribeToMangaInitializer()
}
/**
* Subscribes to the initializer of manga details and updates the view if needed.
*/
private fun subscribeToMangaInitializer() {
initializerSubscription?.let { remove(it) }
initializerSubscription = mangaDetailSubject.observeOn(Schedulers.io())
.flatMap { Observable.from(it) }
.filter { it.thumbnail_url == null && !it.initialized }
.concatMap { getMangaDetailsObservable(it) }
.onBackpressureBuffer()
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ manga ->
@Suppress("DEPRECATION")
view?.onMangaInitialized(manga)
}, { error ->
Timber.e(error)
})
.apply { add(this) }
}
/**
* Returns a manga from the database for the given manga from network. It creates a new entry
* if the manga is not yet in the database.
*
* @param sManga the manga from the source.
* @return a manga from the database.
*/
private fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga {
var localManga = db.getManga(sManga.url, sourceId).executeAsBlocking()
if (localManga == null) {
val newManga = Manga.create(sManga.url, sManga.title, sourceId)
newManga.copyFrom(sManga)
val result = db.insertManga(newManga).executeAsBlocking()
newManga.id = result.insertedId()
localManga = newManga
}
return localManga
}
/**
* Initialize a list of manga.
*
* @param mangas the list of manga to initialize.
*/
fun initializeMangas(mangas: List<Manga>) {
mangaDetailSubject.onNext(mangas)
}
/**
* Returns an observable of manga that initializes the given manga.
*
* @param manga the manga to initialize.
* @return an observable of the manga to initialize
*/
private fun getMangaDetailsObservable(manga: Manga): Observable<Manga> {
return source.fetchMangaDetails(manga)
.flatMap { networkManga ->
manga.copyFrom(networkManga)
manga.initialized = true
db.insertManga(manga).executeAsBlocking()
Observable.just(manga)
}
.onErrorResumeNext { Observable.just(manga) }
}
/**
* Adds or removes a manga from the library.
*
* @param manga the manga to update.
*/
fun changeMangaFavorite(manga: Manga) {
manga.favorite = !manga.favorite
if (!manga.favorite) {
coverCache.deleteFromCache(manga.thumbnail_url)
}
db.insertManga(manga).executeAsBlocking()
}
/**
* Changes the active display mode.
*/
fun swapDisplayMode() {
prefs.catalogueAsList().set(!isListMode)
}
/**
* Set the filter states for the current source.
*
* @param filters a list of active filters.
*/
fun setSourceFilter(filters: FilterList) {
restartPager(filters = filters)
}
open fun createPager(query: String, filters: FilterList): Pager {
return CataloguePager(source, query, filters)
}
private fun FilterList.toItems(): List<IFlexible<*>> {
return mapNotNull {
when (it) {
is Filter.Header -> HeaderItem(it)
is Filter.Separator -> SeparatorItem(it)
is Filter.CheckBox -> CheckboxItem(it)
is Filter.TriState -> TriStateItem(it)
is Filter.Text -> TextItem(it)
is Filter.Select<*> -> SelectItem(it)
is Filter.Group<*> -> {
val group = GroupItem(it)
val subItems = it.state.mapNotNull {
when (it) {
is Filter.CheckBox -> CheckboxSectionItem(it)
is Filter.TriState -> TriStateSectionItem(it)
is Filter.Text -> TextSectionItem(it)
is Filter.Select<*> -> SelectSectionItem(it)
else -> null
} as? ISectionable<*, *>
}
subItems.forEach { it.header = group }
group.subItems = subItems
group
}
is Filter.Sort -> {
val group = SortGroup(it)
val subItems = it.values.map {
SortItem(it, group)
}
group.subItems = subItems
group
}
val map = TreeMap<String, MutableList<CatalogueSource>> { d1, d2 ->
// Catalogues without a lang defined will be placed at the end
when {
d1 == "" && d2 != "" -> 1
d2 == "" && d1 != "" -> -1
else -> d1.compareTo(d2)
}
}
}
/**
* Get the default, and user categories.
*
* @return List of categories, default plus user categories
*/
fun getCategories(): List<Category> {
return db.getCategories().executeAsBlocking()
}
/**
* Gets the category id's the manga is in, if the manga is not in a category, returns the default id.
*
* @param manga the manga to get categories from.
* @return Array of category ids the manga is in, if none returns default id
*/
fun getMangaCategoryIds(manga: Manga): Array<Int?> {
val categories = db.getCategoriesForManga(manga).executeAsBlocking()
return categories.mapNotNull { it.id }.toTypedArray()
}
/**
* Move the given manga to categories.
*
* @param categories the selected categories.
* @param manga the manga to move.
*/
private fun moveMangaToCategories(manga: Manga, categories: List<Category>) {
val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) }
db.setMangaCategories(mc, listOf(manga))
}
/**
* Move the given manga to the category.
*
* @param category the selected category.
* @param manga the manga to move.
*/
fun moveMangaToCategory(manga: Manga, category: Category?) {
moveMangaToCategories(manga, listOfNotNull(category))
}
/**
* Update manga to use selected categories.
*
* @param manga needed to change
* @param selectedCategories selected categories
*/
fun updateMangaCategories(manga: Manga, selectedCategories: List<Category>) {
if (!selectedCategories.isEmpty()) {
if (!manga.favorite)
changeMangaFavorite(manga)
moveMangaToCategories(manga, selectedCategories.filter { it.id != 0 })
} else {
changeMangaFavorite(manga)
val byLang = sources.groupByTo(map, { it.lang })
val sourceItems = byLang.flatMap {
val langItem = LangItem(it.key)
it.value.map { source -> SourceItem(source, langItem) }
}
sourceSubscription = Observable.just(sourceItems)
.subscribeLatestCache(CatalogueController::setSources)
}
private fun loadLastUsedSource() {
val sharedObs = preferences.lastUsedCatalogueSource().asObservable().share()
// Emit the first item immediately but delay subsequent emissions by 500ms.
Observable.merge(
sharedObs.take(1),
sharedObs.skip(1).delay(500, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread()))
.distinctUntilChanged()
.map { (sourceManager.get(it) as? CatalogueSource)?.let { SourceItem(it) } }
.subscribeLatestCache(CatalogueController::setLastUsedSource)
}
fun updateSources() {
sources = getEnabledSources()
loadSources()
}
/**
* Returns a list of enabled sources ordered by language and name.
*
* @return list containing enabled sources.
*/
private fun getEnabledSources(): List<CatalogueSource> {
val languages = preferences.enabledLanguages().getOrDefault()
val hiddenCatalogues = preferences.hiddenCatalogues().getOrDefault()
return sourceManager.getCatalogueSources()
.filter { it.lang in languages }
.filterNot { it.id.toString() in hiddenCatalogues }
.sortedBy { "(${it.lang}) ${it.name}" } +
sourceManager.get(LocalSource.ID) as LocalSource
}
}

View File

@ -1,21 +1,22 @@
package eu.kanade.tachiyomi.ui.catalogue.main
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
import kotlinx.android.synthetic.main.catalogue_main_controller_card.view.*
import java.util.*
class LangHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter, true) {
fun bind(item: LangItem) {
itemView.title.text = when {
item.code == "" -> itemView.context.getString(R.string.other_source)
else -> {
val locale = Locale(item.code)
locale.getDisplayName(locale).capitalize()
}
}
}
package eu.kanade.tachiyomi.ui.catalogue
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import kotlinx.android.synthetic.main.catalogue_main_controller_card.*
import java.util.*
class LangHolder(view: View, adapter: FlexibleAdapter<*>) :
BaseFlexibleViewHolder(view, adapter, true) {
fun bind(item: LangItem) {
title.text = when {
item.code == "" -> itemView.context.getString(R.string.other_source)
else -> {
val locale = Locale(item.code)
locale.getDisplayName(locale).capitalize()
}
}
}
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.catalogue.main
package eu.kanade.tachiyomi.ui.catalogue
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter

View File

@ -1,3 +0,0 @@
package eu.kanade.tachiyomi.ui.catalogue
class NoResultsException : Exception()

View File

@ -1,47 +1,44 @@
package eu.kanade.tachiyomi.ui.catalogue.main
import android.content.Context
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.support.v7.widget.RecyclerView
import android.view.View
class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
private val divider: Drawable
init {
val a = context.obtainStyledAttributes(ATTRS)
divider = a.getDrawable(0)
a.recycle()
}
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
val left = parent.paddingLeft + SourceHolder.margin
val right = parent.width - parent.paddingRight - SourceHolder.margin
val childCount = parent.childCount
for (i in 0 until childCount - 1) {
val child = parent.getChildAt(i)
if (parent.getChildViewHolder(child) is SourceHolder &&
parent.getChildViewHolder(parent.getChildAt(i + 1)) is SourceHolder) {
val params = child.layoutParams as RecyclerView.LayoutParams
val top = child.bottom + params.bottomMargin
val bottom = top + divider.intrinsicHeight
divider.setBounds(left, top, right, bottom)
divider.draw(c)
}
}
}
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView,
state: RecyclerView.State) {
outRect.set(0, 0, 0, divider.intrinsicHeight)
}
companion object {
private val ATTRS = intArrayOf(android.R.attr.listDivider)
}
package eu.kanade.tachiyomi.ui.catalogue
import android.content.Context
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.support.v7.widget.RecyclerView
import android.view.View
class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
private val divider: Drawable
init {
val a = context.obtainStyledAttributes(intArrayOf(android.R.attr.listDivider))
divider = a.getDrawable(0)
a.recycle()
}
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
val left = parent.paddingLeft + SourceHolder.margin
val right = parent.width - parent.paddingRight - SourceHolder.margin
val childCount = parent.childCount
for (i in 0 until childCount - 1) {
val child = parent.getChildAt(i)
if (parent.getChildViewHolder(child) is SourceHolder &&
parent.getChildViewHolder(parent.getChildAt(i + 1)) is SourceHolder) {
val params = child.layoutParams as RecyclerView.LayoutParams
val top = child.bottom + params.bottomMargin
val bottom = top + divider.intrinsicHeight
divider.setBounds(left, top, right, bottom)
divider.draw(c)
}
}
}
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView,
state: RecyclerView.State) {
outRect.set(0, 0, 0, divider.intrinsicHeight)
}
}

View File

@ -1,55 +1,53 @@
package eu.kanade.tachiyomi.ui.catalogue.main
package eu.kanade.tachiyomi.ui.catalogue
import android.os.Build
import android.view.View
import android.view.ViewGroup
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.online.LoginSource
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.dpToPx
import eu.kanade.tachiyomi.util.getRound
import eu.kanade.tachiyomi.util.gone
import eu.kanade.tachiyomi.util.visible
import io.github.mthli.slice.Slice
import kotlinx.android.synthetic.main.catalogue_main_controller_card_item.view.*
import kotlinx.android.synthetic.main.catalogue_main_controller_card_item.*
class SourceHolder(view: View, adapter: CatalogueMainAdapter) : FlexibleViewHolder(view, adapter) {
class SourceHolder(view: View, adapter: CatalogueAdapter) : BaseFlexibleViewHolder(view, adapter) {
private val slice = Slice(itemView.card).apply {
private val slice = Slice(card).apply {
setColor(adapter.cardBackground)
}
init {
itemView.source_browse.setOnClickListener {
source_browse.setOnClickListener {
adapter.browseClickListener.onBrowseClick(adapterPosition)
}
itemView.source_latest.setOnClickListener {
source_latest.setOnClickListener {
adapter.latestClickListener.onLatestClick(adapterPosition)
}
}
fun bind(item: SourceItem) {
val source = item.source
with(itemView) {
setCardEdges(item)
setCardEdges(item)
// Set source name
title.text = source.name
// Set source name
title.text = source.name
// Set circle letter image.
post {
image.setImageDrawable(image.getRound(source.name.take(1).toUpperCase(),false))
}
// Set circle letter image.
itemView.post {
image.setImageDrawable(image.getRound(source.name.take(1).toUpperCase(),false))
}
// If source is login, show only login option
if (source is LoginSource && !source.isLogged()) {
source_browse.setText(R.string.login)
source_latest.gone()
} else {
source_browse.setText(R.string.browse)
source_latest.visible()
}
// If source is login, show only login option
if (source is LoginSource && !source.isLogged()) {
source_browse.setText(R.string.login)
source_latest.gone()
} else {
source_browse.setText(R.string.browse)
source_latest.visible()
}
}
@ -94,7 +92,7 @@ class SourceHolder(view: View, adapter: CatalogueMainAdapter) : FlexibleViewHold
}
private fun setMargins(left: Int, top: Int, right: Int, bottom: Int) {
val v = itemView.card
val v = card
if (v.layoutParams is ViewGroup.MarginLayoutParams) {
val p = v.layoutParams as ViewGroup.MarginLayoutParams
p.setMargins(left, top, right, bottom)

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.catalogue.main
package eu.kanade.tachiyomi.ui.catalogue
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
@ -26,7 +26,7 @@ data class SourceItem(val source: CatalogueSource, val header: LangItem? = null)
* Creates a new view holder for this item.
*/
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): SourceHolder {
return SourceHolder(view, adapter as CatalogueMainAdapter)
return SourceHolder(view, adapter as CatalogueAdapter)
}
/**

View File

@ -0,0 +1,522 @@
package eu.kanade.tachiyomi.ui.catalogue.browse
import android.content.res.Configuration
import android.os.Bundle
import android.support.design.widget.Snackbar
import android.support.v4.widget.DrawerLayout
import android.support.v7.widget.*
import android.view.*
import com.afollestad.materialdialogs.MaterialDialog
import com.f2prateek.rx.preferences.Preference
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.*
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import eu.kanade.tachiyomi.widget.DrawerSwipeCloseListener
import kotlinx.android.synthetic.main.catalogue_controller.*
import kotlinx.android.synthetic.main.main_activity.*
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.subscriptions.Subscriptions
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
import java.util.concurrent.TimeUnit
/**
* Controller to manage the catalogues available in the app.
*/
open class BrowseCatalogueController(bundle: Bundle) :
NucleusController<BrowseCataloguePresenter>(bundle),
SecondaryDrawerController,
FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener,
FlexibleAdapter.EndlessScrollListener,
ChangeMangaCategoriesDialog.Listener {
constructor(source: CatalogueSource) : this(Bundle().apply {
putLong(SOURCE_ID_KEY, source.id)
})
/**
* Preferences helper.
*/
private val preferences: PreferencesHelper by injectLazy()
/**
* Adapter containing the list of manga from the catalogue.
*/
private var adapter: FlexibleAdapter<IFlexible<*>>? = null
/**
* Snackbar containing an error message when a request fails.
*/
private var snack: Snackbar? = null
/**
* Navigation view containing filter items.
*/
private var navView: CatalogueNavigationView? = null
/**
* Recycler view with the list of results.
*/
private var recycler: RecyclerView? = null
/**
* Drawer listener to allow swipe only for closing the drawer.
*/
private var drawerListener: DrawerLayout.DrawerListener? = null
/**
* Subscription for the search view.
*/
private var searchViewSubscription: Subscription? = null
/**
* Subscription for the number of manga per row.
*/
private var numColumnsSubscription: Subscription? = null
/**
* Endless loading item.
*/
private var progressItem: ProgressItem? = null
init {
setHasOptionsMenu(true)
}
override fun getTitle(): String? {
return presenter.source.name
}
override fun createPresenter(): BrowseCataloguePresenter {
return BrowseCataloguePresenter(args.getLong(SOURCE_ID_KEY))
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
return inflater.inflate(R.layout.catalogue_controller, container, false)
}
override fun onViewCreated(view: View) {
super.onViewCreated(view)
// Initialize adapter, scroll listener and recycler views
adapter = FlexibleAdapter(null, this)
setupRecycler(view)
navView?.setFilters(presenter.filterItems)
progress?.visible()
}
override fun onDestroyView(view: View) {
numColumnsSubscription?.unsubscribe()
numColumnsSubscription = null
searchViewSubscription?.unsubscribe()
searchViewSubscription = null
adapter = null
snack = null
recycler = null
super.onDestroyView(view)
}
override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup? {
// Inflate and prepare drawer
val navView = drawer.inflate(R.layout.catalogue_drawer) as CatalogueNavigationView
this.navView = navView
drawerListener = DrawerSwipeCloseListener(drawer, navView).also {
drawer.addDrawerListener(it)
}
navView.setFilters(presenter.filterItems)
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.END)
navView.onSearchClicked = {
val allDefault = presenter.sourceFilters == presenter.source.getFilterList()
showProgressBar()
adapter?.clear()
presenter.setSourceFilter(if (allDefault) FilterList() else presenter.sourceFilters)
}
navView.onResetClicked = {
presenter.appliedFilters = FilterList()
val newFilters = presenter.source.getFilterList()
presenter.sourceFilters = newFilters
navView.setFilters(presenter.filterItems)
}
return navView
}
override fun cleanupSecondaryDrawer(drawer: DrawerLayout) {
drawerListener?.let { drawer.removeDrawerListener(it) }
drawerListener = null
navView = null
}
private fun setupRecycler(view: View) {
numColumnsSubscription?.unsubscribe()
var oldPosition = RecyclerView.NO_POSITION
val oldRecycler = catalogue_view?.getChildAt(1)
if (oldRecycler is RecyclerView) {
oldPosition = (oldRecycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
oldRecycler.adapter = null
catalogue_view?.removeView(oldRecycler)
}
val recycler = if (presenter.isListMode) {
RecyclerView(view.context).apply {
id = R.id.recycler
layoutManager = LinearLayoutManager(context)
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
}
} else {
(catalogue_view.inflate(R.layout.catalogue_recycler_autofit) as AutofitRecyclerView).apply {
numColumnsSubscription = getColumnsPreferenceForCurrentOrientation().asObservable()
.doOnNext { spanCount = it }
.skip(1)
// Set again the adapter to recalculate the covers height
.subscribe { adapter = this@BrowseCatalogueController.adapter }
(layoutManager as GridLayoutManager).spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return when (adapter?.getItemViewType(position)) {
R.layout.catalogue_grid_item, null -> 1
else -> spanCount
}
}
}
}
}
recycler.setHasFixedSize(true)
recycler.adapter = adapter
catalogue_view.addView(recycler, 1)
if (oldPosition != RecyclerView.NO_POSITION) {
recycler.layoutManager.scrollToPosition(oldPosition)
}
this.recycler = recycler
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.catalogue_list, menu)
// Initialize search menu
menu.findItem(R.id.action_search).apply {
val searchView = actionView as SearchView
val query = presenter.query
if (!query.isBlank()) {
expandActionView()
searchView.setQuery(query, true)
searchView.clearFocus()
}
val searchEventsObservable = searchView.queryTextChangeEvents()
.skip(1)
.share()
val writingObservable = searchEventsObservable
.filter { !it.isSubmitted }
.debounce(1250, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
val submitObservable = searchEventsObservable
.filter { it.isSubmitted }
searchViewSubscription?.unsubscribe()
searchViewSubscription = Observable.merge(writingObservable, submitObservable)
.map { it.queryText().toString() }
.distinctUntilChanged()
.subscribeUntilDestroy { searchWithQuery(it) }
untilDestroySubscriptions.add(
Subscriptions.create { if (isActionViewExpanded) collapseActionView() })
}
// Setup filters button
menu.findItem(R.id.action_set_filter).apply {
icon.mutate()
if (presenter.sourceFilters.isEmpty()) {
isEnabled = false
icon.alpha = 128
} else {
isEnabled = true
icon.alpha = 255
}
}
// Show next display mode
menu.findItem(R.id.action_display_mode).apply {
val icon = if (presenter.isListMode)
R.drawable.ic_view_module_white_24dp
else
R.drawable.ic_view_list_white_24dp
setIcon(icon)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_display_mode -> swapDisplayMode()
R.id.action_set_filter -> navView?.let { activity?.drawer?.openDrawer(Gravity.END) }
else -> return super.onOptionsItemSelected(item)
}
return true
}
/**
* Restarts the request with a new query.
*
* @param newQuery the new query.
*/
private fun searchWithQuery(newQuery: String) {
// If text didn't change, do nothing
if (presenter.query == newQuery)
return
// FIXME dirty fix to restore the toolbar buttons after closing search mode.
if (newQuery == "") {
activity?.invalidateOptionsMenu()
}
showProgressBar()
adapter?.clear()
presenter.restartPager(newQuery)
}
/**
* Called from the presenter when the network request is received.
*
* @param page the current page.
* @param mangas the list of manga of the page.
*/
fun onAddPage(page: Int, mangas: List<CatalogueItem>) {
val adapter = adapter ?: return
hideProgressBar()
if (page == 1) {
adapter.clear()
resetProgressItem()
}
adapter.onLoadMoreComplete(mangas)
}
/**
* Called from the presenter when the network request fails.
*
* @param error the error received.
*/
fun onAddPageError(error: Throwable) {
Timber.e(error)
val adapter = adapter ?: return
adapter.onLoadMoreComplete(null)
hideProgressBar()
val message = if (error is NoResultsException) "No results found" else (error.message ?: "")
snack?.dismiss()
snack = catalogue_view?.snack(message, Snackbar.LENGTH_INDEFINITE) {
setAction(R.string.action_retry) {
// If not the first page, show bottom progress bar.
if (adapter.mainItemCount > 0) {
val item = progressItem ?: return@setAction
adapter.addScrollableFooterWithDelay(item, 0, true)
} else {
showProgressBar()
}
presenter.requestNext()
}
}
}
/**
* Sets a new progress item and reenables the scroll listener.
*/
private fun resetProgressItem() {
progressItem = ProgressItem()
adapter?.endlessTargetCount = 0
adapter?.setEndlessScrollListener(this, progressItem!!)
}
/**
* Called by the adapter when scrolled near the bottom.
*/
override fun onLoadMore(lastPosition: Int, currentPage: Int) {
if (presenter.hasNextPage()) {
presenter.requestNext()
} else {
adapter?.onLoadMoreComplete(null)
adapter?.endlessTargetCount = 1
}
}
override fun noMoreLoad(newItemsSize: Int) {
}
/**
* Called from the presenter when a manga is initialized.
*
* @param manga the manga initialized
*/
fun onMangaInitialized(manga: Manga) {
getHolder(manga)?.setImage(manga)
}
/**
* Swaps the current display mode.
*/
fun swapDisplayMode() {
val view = view ?: return
val adapter = adapter ?: return
presenter.swapDisplayMode()
val isListMode = presenter.isListMode
activity?.invalidateOptionsMenu()
setupRecycler(view)
if (!isListMode || !view.context.connectivityManager.isActiveNetworkMetered) {
// Initialize mangas if going to grid view or if over wifi when going to list view
val mangas = (0 until adapter.itemCount).mapNotNull {
(adapter.getItem(it) as? CatalogueItem)?.manga
}
presenter.initializeMangas(mangas)
}
}
/**
* 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)
preferences.portraitColumns()
else
preferences.landscapeColumns()
}
/**
* Returns the view holder for the given manga.
*
* @param manga the manga to find.
* @return the holder of the manga or null if it's not bound.
*/
private fun getHolder(manga: Manga): CatalogueHolder? {
val adapter = adapter ?: return null
adapter.allBoundViewHolders.forEach { holder ->
val item = adapter.getItem(holder.adapterPosition) as? CatalogueItem
if (item != null && item.manga.id!! == manga.id!!) {
return holder as CatalogueHolder
}
}
return null
}
/**
* Shows the progress bar.
*/
private fun showProgressBar() {
progress?.visible()
snack?.dismiss()
snack = null
}
/**
* Hides active progress bars.
*/
private fun hideProgressBar() {
progress?.gone()
}
/**
* Called when a manga is clicked.
*
* @param position the position of the element clicked.
* @return true if the item should be selected, false otherwise.
*/
override fun onItemClick(position: Int): Boolean {
val item = adapter?.getItem(position) as? CatalogueItem ?: return false
router.pushController(MangaController(item.manga, true).withFadeTransaction())
return false
}
/**
* Called when a manga is long clicked.
*
* Adds the manga to the default category if none is set it shows a list of categories for the user to put the manga
* in, the list consists of the default category plus the user's categories. The default category is preselected on
* new manga, and on already favorited manga the manga's categories are preselected.
*
* @param position the position of the element clicked.
*/
override fun onItemLongClick(position: Int) {
val activity = activity ?: return
val manga = (adapter?.getItem(position) as? CatalogueItem?)?.manga ?: return
if (manga.favorite) {
MaterialDialog.Builder(activity)
.items(activity.getString(R.string.remove_from_library))
.itemsCallback { _, _, which, _ ->
when (which) {
0 -> {
presenter.changeMangaFavorite(manga)
adapter?.notifyItemChanged(position)
activity?.toast(activity?.getString(R.string.manga_removed_library))
}
}
}.show()
} else {
presenter.changeMangaFavorite(manga)
adapter?.notifyItemChanged(position)
val categories = presenter.getCategories()
val defaultCategory = categories.find { it.id == preferences.defaultCategory() }
if (defaultCategory != null) {
presenter.moveMangaToCategory(manga, defaultCategory)
} else if (categories.size <= 1) { // default or the one from the user
presenter.moveMangaToCategory(manga, categories.firstOrNull())
} else {
val ids = presenter.getMangaCategoryIds(manga)
val preselected = ids.mapNotNull { id ->
categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
}.toTypedArray()
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
.showDialog(router)
}
activity?.toast(activity?.getString(R.string.manga_added_library))
}
}
/**
* Update manga to use selected categories.
*
* @param mangas The list of manga to move to categories.
* @param categories The list of categories where manga will be placed.
*/
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
val manga = mangas.firstOrNull() ?: return
presenter.updateMangaCategories(manga, categories)
}
protected companion object {
const val SOURCE_ID_KEY = "sourceId"
}
}

View File

@ -0,0 +1,376 @@
package eu.kanade.tachiyomi.ui.catalogue.browse
import android.os.Bundle
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.flexibleadapter.items.ISectionable
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.catalogue.filter.*
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import rx.subjects.PublishSubject
import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* Presenter of [BrowseCatalogueController].
*/
open class BrowseCataloguePresenter(
sourceId: Long,
sourceManager: SourceManager = Injekt.get(),
private val db: DatabaseHelper = Injekt.get(),
private val prefs: PreferencesHelper = Injekt.get(),
private val coverCache: CoverCache = Injekt.get()
) : BasePresenter<BrowseCatalogueController>() {
/**
* Selected source.
*/
val source = sourceManager.get(sourceId) as CatalogueSource
/**
* Query from the view.
*/
var query = ""
private set
/**
* Modifiable list of filters.
*/
var sourceFilters = FilterList()
set(value) {
field = value
filterItems = value.toItems()
}
var filterItems: List<IFlexible<*>> = emptyList()
/**
* List of filters used by the [Pager]. If empty alongside [query], the popular query is used.
*/
var appliedFilters = FilterList()
/**
* Pager containing a list of manga results.
*/
private lateinit var pager: Pager
/**
* Subject that initializes a list of manga.
*/
private val mangaDetailSubject = PublishSubject.create<List<Manga>>()
/**
* Whether the view is in list mode or not.
*/
var isListMode: Boolean = false
private set
/**
* Subscription for the pager.
*/
private var pagerSubscription: Subscription? = null
/**
* Subscription for one request from the pager.
*/
private var pageSubscription: Subscription? = null
/**
* Subscription to initialize manga details.
*/
private var initializerSubscription: Subscription? = null
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
sourceFilters = source.getFilterList()
if (savedState != null) {
query = savedState.getString(::query.name, "")
}
add(prefs.catalogueAsList().asObservable()
.subscribe { setDisplayMode(it) })
restartPager()
}
override fun onSave(state: Bundle) {
state.putString(::query.name, query)
super.onSave(state)
}
/**
* Restarts the pager for the active source with the provided query and filters.
*
* @param query the query.
* @param filters the current state of the filters (for search mode).
*/
fun restartPager(query: String = this.query, filters: FilterList = this.appliedFilters) {
this.query = query
this.appliedFilters = filters
subscribeToMangaInitializer()
// Create a new pager.
pager = createPager(query, filters)
val sourceId = source.id
val catalogueAsList = prefs.catalogueAsList()
// Prepare the pager.
pagerSubscription?.let { remove(it) }
pagerSubscription = pager.results()
.observeOn(Schedulers.io())
.map { it.first to it.second.map { networkToLocalManga(it, sourceId) } }
.doOnNext { initializeMangas(it.second) }
.map { it.first to it.second.map { CatalogueItem(it, catalogueAsList) } }
.observeOn(AndroidSchedulers.mainThread())
.subscribeReplay({ view, (page, mangas) ->
view.onAddPage(page, mangas)
}, { _, error ->
Timber.e(error)
})
// Request first page.
requestNext()
}
/**
* Requests the next page for the active pager.
*/
fun requestNext() {
if (!hasNextPage()) return
pageSubscription?.let { remove(it) }
pageSubscription = Observable.defer { pager.requestNext() }
.subscribeFirst({ _, _ ->
// Nothing to do when onNext is emitted.
}, BrowseCatalogueController::onAddPageError)
}
/**
* Returns true if the last fetched page has a next page.
*/
fun hasNextPage(): Boolean {
return pager.hasNextPage
}
/**
* Sets the display mode.
*
* @param asList whether the current mode is in list or not.
*/
private fun setDisplayMode(asList: Boolean) {
isListMode = asList
subscribeToMangaInitializer()
}
/**
* Subscribes to the initializer of manga details and updates the view if needed.
*/
private fun subscribeToMangaInitializer() {
initializerSubscription?.let { remove(it) }
initializerSubscription = mangaDetailSubject.observeOn(Schedulers.io())
.flatMap { Observable.from(it) }
.filter { it.thumbnail_url == null && !it.initialized }
.concatMap { getMangaDetailsObservable(it) }
.onBackpressureBuffer()
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ manga ->
@Suppress("DEPRECATION")
view?.onMangaInitialized(manga)
}, { error ->
Timber.e(error)
})
.apply { add(this) }
}
/**
* Returns a manga from the database for the given manga from network. It creates a new entry
* if the manga is not yet in the database.
*
* @param sManga the manga from the source.
* @return a manga from the database.
*/
private fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga {
var localManga = db.getManga(sManga.url, sourceId).executeAsBlocking()
if (localManga == null) {
val newManga = Manga.create(sManga.url, sManga.title, sourceId)
newManga.copyFrom(sManga)
val result = db.insertManga(newManga).executeAsBlocking()
newManga.id = result.insertedId()
localManga = newManga
}
return localManga
}
/**
* Initialize a list of manga.
*
* @param mangas the list of manga to initialize.
*/
fun initializeMangas(mangas: List<Manga>) {
mangaDetailSubject.onNext(mangas)
}
/**
* Returns an observable of manga that initializes the given manga.
*
* @param manga the manga to initialize.
* @return an observable of the manga to initialize
*/
private fun getMangaDetailsObservable(manga: Manga): Observable<Manga> {
return source.fetchMangaDetails(manga)
.flatMap { networkManga ->
manga.copyFrom(networkManga)
manga.initialized = true
db.insertManga(manga).executeAsBlocking()
Observable.just(manga)
}
.onErrorResumeNext { Observable.just(manga) }
}
/**
* Adds or removes a manga from the library.
*
* @param manga the manga to update.
*/
fun changeMangaFavorite(manga: Manga) {
manga.favorite = !manga.favorite
if (!manga.favorite) {
coverCache.deleteFromCache(manga.thumbnail_url)
}
db.insertManga(manga).executeAsBlocking()
}
/**
* Changes the active display mode.
*/
fun swapDisplayMode() {
prefs.catalogueAsList().set(!isListMode)
}
/**
* Set the filter states for the current source.
*
* @param filters a list of active filters.
*/
fun setSourceFilter(filters: FilterList) {
restartPager(filters = filters)
}
open fun createPager(query: String, filters: FilterList): Pager {
return CataloguePager(source, query, filters)
}
private fun FilterList.toItems(): List<IFlexible<*>> {
return mapNotNull {
when (it) {
is Filter.Header -> HeaderItem(it)
is Filter.Separator -> SeparatorItem(it)
is Filter.CheckBox -> CheckboxItem(it)
is Filter.TriState -> TriStateItem(it)
is Filter.Text -> TextItem(it)
is Filter.Select<*> -> SelectItem(it)
is Filter.Group<*> -> {
val group = GroupItem(it)
val subItems = it.state.mapNotNull {
when (it) {
is Filter.CheckBox -> CheckboxSectionItem(it)
is Filter.TriState -> TriStateSectionItem(it)
is Filter.Text -> TextSectionItem(it)
is Filter.Select<*> -> SelectSectionItem(it)
else -> null
} as? ISectionable<*, *>
}
subItems.forEach { it.header = group }
group.subItems = subItems
group
}
is Filter.Sort -> {
val group = SortGroup(it)
val subItems = it.values.map {
SortItem(it, group)
}
group.subItems = subItems
group
}
}
}
}
/**
* Get the default, and user categories.
*
* @return List of categories, default plus user categories
*/
fun getCategories(): List<Category> {
return db.getCategories().executeAsBlocking()
}
/**
* Gets the category id's the manga is in, if the manga is not in a category, returns the default id.
*
* @param manga the manga to get categories from.
* @return Array of category ids the manga is in, if none returns default id
*/
fun getMangaCategoryIds(manga: Manga): Array<Int?> {
val categories = db.getCategoriesForManga(manga).executeAsBlocking()
return categories.mapNotNull { it.id }.toTypedArray()
}
/**
* Move the given manga to categories.
*
* @param categories the selected categories.
* @param manga the manga to move.
*/
private fun moveMangaToCategories(manga: Manga, categories: List<Category>) {
val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) }
db.setMangaCategories(mc, listOf(manga))
}
/**
* Move the given manga to the category.
*
* @param category the selected category.
* @param manga the manga to move.
*/
fun moveMangaToCategory(manga: Manga, category: Category?) {
moveMangaToCategories(manga, listOfNotNull(category))
}
/**
* Update manga to use selected categories.
*
* @param manga needed to change
* @param selectedCategories selected categories
*/
fun updateMangaCategories(manga: Manga, selectedCategories: List<Category>) {
if (!selectedCategories.isEmpty()) {
if (!manga.favorite)
changeMangaFavorite(manga)
moveMangaToCategories(manga, selectedCategories.filter { it.id != 0 })
} else {
changeMangaFavorite(manga)
}
}
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.catalogue
package eu.kanade.tachiyomi.ui.catalogue.browse
import android.view.View
import com.bumptech.glide.load.engine.DiskCacheStrategy
@ -6,7 +6,7 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.widget.StateImageViewTarget
import kotlinx.android.synthetic.main.catalogue_grid_item.view.*
import kotlinx.android.synthetic.main.catalogue_grid_item.*
/**
* Class used to hold the displayed data of a manga in the catalogue, like the cover or the title.
@ -27,16 +27,16 @@ class CatalogueGridHolder(private val view: View, private val adapter: FlexibleA
*/
override fun onSetValues(manga: Manga) {
// Set manga title
view.title.text = manga.title
title.text = manga.title
// Set alpha of thumbnail.
view.thumbnail.alpha = if (manga.favorite) 0.3f else 1.0f
thumbnail.alpha = if (manga.favorite) 0.3f else 1.0f
setImage(manga)
}
override fun setImage(manga: Manga) {
GlideApp.with(view.context).clear(view.thumbnail)
GlideApp.with(view.context).clear(thumbnail)
if (!manga.thumbnail_url.isNullOrEmpty()) {
GlideApp.with(view.context)
.load(manga)
@ -44,7 +44,7 @@ class CatalogueGridHolder(private val view: View, private val adapter: FlexibleA
.centerCrop()
.skipMemoryCache(true)
.placeholder(android.R.color.transparent)
.into(StateImageViewTarget(view.thumbnail, view.progress))
.into(StateImageViewTarget(thumbnail, progress))
}
}
}

View File

@ -1,9 +1,9 @@
package eu.kanade.tachiyomi.ui.catalogue
package eu.kanade.tachiyomi.ui.catalogue.browse
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
/**
* Generic class used to hold the displayed data of a manga in the catalogue.
@ -12,7 +12,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
* @param adapter the adapter handling this holder.
*/
abstract class CatalogueHolder(view: View, adapter: FlexibleAdapter<*>) :
FlexibleViewHolder(view, adapter) {
BaseFlexibleViewHolder(view, adapter) {
/**
* Method called from [CatalogueAdapter.onBindViewHolder]. It updates the data for this

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.catalogue
package eu.kanade.tachiyomi.ui.catalogue.browse
import android.view.Gravity
import android.view.View

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.catalogue
package eu.kanade.tachiyomi.ui.catalogue.browse
import android.view.View
import com.bumptech.glide.load.engine.DiskCacheStrategy
@ -6,7 +6,7 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.util.getResourceColor
import kotlinx.android.synthetic.main.catalogue_list_item.view.*
import kotlinx.android.synthetic.main.catalogue_list_item.*
/**
* Class used to hold the displayed data of a manga in the catalogue, like the cover or the title.
@ -29,14 +29,14 @@ class CatalogueListHolder(private val view: View, adapter: FlexibleAdapter<*>) :
* @param manga the manga to bind.
*/
override fun onSetValues(manga: Manga) {
view.title.text = manga.title
view.title.setTextColor(if (manga.favorite) favoriteColor else unfavoriteColor)
title.text = manga.title
title.setTextColor(if (manga.favorite) favoriteColor else unfavoriteColor)
setImage(manga)
}
override fun setImage(manga: Manga) {
GlideApp.with(view.context).clear(view.thumbnail)
GlideApp.with(view.context).clear(thumbnail)
if (!manga.thumbnail_url.isNullOrEmpty()) {
GlideApp.with(view.context)
.load(manga)
@ -46,7 +46,7 @@ class CatalogueListHolder(private val view: View, adapter: FlexibleAdapter<*>) :
.dontAnimate()
.skipMemoryCache(true)
.placeholder(android.R.color.transparent)
.into(view.thumbnail)
.into(thumbnail)
}
}

View File

@ -1,40 +1,40 @@
package eu.kanade.tachiyomi.ui.catalogue
import android.content.Context
import android.util.AttributeSet
import android.view.ViewGroup
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.inflate
import eu.kanade.tachiyomi.widget.SimpleNavigationView
import kotlinx.android.synthetic.main.catalogue_drawer_content.view.*
class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null)
: SimpleNavigationView(context, attrs) {
val adapter: FlexibleAdapter<IFlexible<*>> = FlexibleAdapter<IFlexible<*>>(null)
.setDisplayHeadersAtStartUp(true)
.setStickyHeaders(true)
var onSearchClicked = {}
var onResetClicked = {}
init {
recycler.adapter = adapter
recycler.setHasFixedSize(true)
val view = inflate(R.layout.catalogue_drawer_content)
((view as ViewGroup).getChildAt(1) as ViewGroup).addView(recycler)
addView(view)
search_btn.setOnClickListener { onSearchClicked() }
reset_btn.setOnClickListener { onResetClicked() }
}
fun setFilters(items: List<IFlexible<*>>) {
adapter.updateDataSet(items)
}
package eu.kanade.tachiyomi.ui.catalogue.browse
import android.content.Context
import android.util.AttributeSet
import android.view.ViewGroup
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.inflate
import eu.kanade.tachiyomi.widget.SimpleNavigationView
import kotlinx.android.synthetic.main.catalogue_drawer_content.view.*
class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null)
: SimpleNavigationView(context, attrs) {
val adapter: FlexibleAdapter<IFlexible<*>> = FlexibleAdapter<IFlexible<*>>(null)
.setDisplayHeadersAtStartUp(true)
.setStickyHeaders(true)
var onSearchClicked = {}
var onResetClicked = {}
init {
recycler.adapter = adapter
recycler.setHasFixedSize(true)
val view = inflate(R.layout.catalogue_drawer_content)
((view as ViewGroup).getChildAt(1) as ViewGroup).addView(recycler)
addView(view)
search_btn.setOnClickListener { onSearchClicked() }
reset_btn.setOnClickListener { onResetClicked() }
}
fun setFilters(items: List<IFlexible<*>>) {
adapter.updateDataSet(items)
}
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.catalogue
package eu.kanade.tachiyomi.ui.catalogue.browse
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList

View File

@ -0,0 +1,3 @@
package eu.kanade.tachiyomi.ui.catalogue.browse
class NoResultsException : Exception()

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.catalogue
package eu.kanade.tachiyomi.ui.catalogue.browse
import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.tachiyomi.source.model.MangasPage

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.catalogue
package eu.kanade.tachiyomi.ui.catalogue.browse
import android.view.View
import android.widget.ProgressBar

View File

@ -2,14 +2,14 @@ package eu.kanade.tachiyomi.ui.catalogue.global_search
import android.view.View
import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.widget.StateImageViewTarget
import kotlinx.android.synthetic.main.catalogue_global_search_controller_card_item.view.*
import kotlinx.android.synthetic.main.catalogue_global_search_controller_card_item.*
class CatalogueSearchCardHolder(view: View, adapter: CatalogueSearchCardAdapter)
: FlexibleViewHolder(view, adapter) {
: BaseFlexibleViewHolder(view, adapter) {
init {
// Call onMangaClickListener when item is pressed.
@ -22,13 +22,13 @@ class CatalogueSearchCardHolder(view: View, adapter: CatalogueSearchCardAdapter)
}
fun bind(manga: Manga) {
itemView.tvTitle.text = manga.title
tvTitle.text = manga.title
setImage(manga)
}
fun setImage(manga: Manga) {
GlideApp.with(itemView.context).clear(itemView.itemImage)
GlideApp.with(itemView.context).clear(itemImage)
if (!manga.thumbnail_url.isNullOrEmpty()) {
GlideApp.with(itemView.context)
.load(manga)
@ -36,7 +36,7 @@ class CatalogueSearchCardHolder(view: View, adapter: CatalogueSearchCardAdapter)
.centerCrop()
.skipMemoryCache(true)
.placeholder(android.R.color.transparent)
.into(StateImageViewTarget(itemView.itemImage, itemView.progress))
.into(StateImageViewTarget(itemImage, progress))
}
}

View File

@ -4,15 +4,14 @@ import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.SearchView
import android.view.*
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.manga.MangaController
import kotlinx.android.synthetic.main.catalogue_global_search_controller.view.*
import kotlinx.android.synthetic.main.catalogue_global_search_controller.*
/**
* This controller shows and manages the different search result in global search.
@ -71,9 +70,7 @@ class CatalogueSearchController(private val initialQuery: String? = null) :
*/
override fun onMangaClick(manga: Manga) {
// Open MangaController.
router.pushController(RouterTransaction.with(MangaController(manga, true))
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler()))
router.pushController(MangaController(manga, true).withFadeTransaction())
}
/**
@ -115,18 +112,15 @@ class CatalogueSearchController(private val initialQuery: String? = null) :
* Called when the view is created
*
* @param view view of controller
* @param savedViewState information from previous state.
*/
override fun onViewCreated(view: View, savedViewState: Bundle?) {
super.onViewCreated(view, savedViewState)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
adapter = CatalogueSearchAdapter(this)
with(view) {
// Create recycler and set adapter.
recycler.layoutManager = LinearLayoutManager(context)
recycler.adapter = adapter
}
// Create recycler and set adapter.
recycler.layoutManager = LinearLayoutManager(view.context)
recycler.adapter = adapter
}
override fun onDestroyView(view: View) {

View File

@ -2,14 +2,14 @@ package eu.kanade.tachiyomi.ui.catalogue.global_search
import android.support.v7.widget.LinearLayoutManager
import android.view.View
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.getResourceColor
import eu.kanade.tachiyomi.util.gone
import eu.kanade.tachiyomi.util.setVectorCompat
import eu.kanade.tachiyomi.util.visible
import kotlinx.android.synthetic.main.catalogue_global_search_controller_card.view.*
import kotlinx.android.synthetic.main.catalogue_global_search_controller_card.*
/**
* Holder that binds the [CatalogueSearchItem] containing catalogue cards.
@ -17,7 +17,8 @@ import kotlinx.android.synthetic.main.catalogue_global_search_controller_card.vi
* @param view view of [CatalogueSearchItem]
* @param adapter instance of [CatalogueSearchAdapter]
*/
class CatalogueSearchHolder(view: View, val adapter: CatalogueSearchAdapter) : FlexibleViewHolder(view, adapter) {
class CatalogueSearchHolder(view: View, val adapter: CatalogueSearchAdapter) :
BaseFlexibleViewHolder(view, adapter) {
/**
* Adapter containing manga from search results.
@ -27,14 +28,12 @@ class CatalogueSearchHolder(view: View, val adapter: CatalogueSearchAdapter) : F
private var lastBoundResults: List<CatalogueSearchCardItem>? = null
init {
with(itemView) {
// Set layout horizontal.
recycler.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
recycler.adapter = mangaAdapter
// Set layout horizontal.
recycler.layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.HORIZONTAL, false)
recycler.adapter = mangaAdapter
nothing_found_icon.setVectorCompat(R.drawable.ic_search_black_112dp,
context.getResourceColor(android.R.attr.textColorHint))
}
nothing_found_icon.setVectorCompat(R.drawable.ic_search_black_112dp,
view.context.getResourceColor(android.R.attr.textColorHint))
}
/**
@ -46,28 +45,26 @@ class CatalogueSearchHolder(view: View, val adapter: CatalogueSearchAdapter) : F
val source = item.source
val results = item.results
with(itemView) {
// Set Title witch country code if available.
title.text = if (!source.lang.isEmpty()) "${source.name} (${source.lang})" else source.name
// Set Title witch country code if available.
title.text = if (!source.lang.isEmpty()) "${source.name} (${source.lang})" else source.name
when {
results == null -> {
progress.visible()
nothing_found.gone()
}
results.isEmpty() -> {
progress.gone()
nothing_found.visible()
}
else -> {
progress.gone()
nothing_found.gone()
}
when {
results == null -> {
progress.visible()
nothing_found.gone()
}
if (results !== lastBoundResults) {
mangaAdapter.updateDataSet(results)
lastBoundResults = results
results.isEmpty() -> {
progress.gone()
nothing_found.visible()
}
else -> {
progress.gone()
nothing_found.gone()
}
}
if (results !== lastBoundResults) {
mangaAdapter.updateDataSet(results)
lastBoundResults = results
}
}

View File

@ -12,7 +12,7 @@ import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.LoginSource
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCataloguePresenter
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
@ -67,7 +67,7 @@ class CatalogueSearchPresenter(
super.onCreate(savedState)
// Perform a search with previous or initial state
search(savedState?.getString(CataloguePresenter::query.name) ?: initialQuery.orEmpty())
search(savedState?.getString(BrowseCataloguePresenter::query.name) ?: initialQuery.orEmpty())
}
override fun onDestroy() {
@ -77,7 +77,7 @@ class CatalogueSearchPresenter(
}
override fun onSave(state: Bundle) {
state.putString(CataloguePresenter::query.name, query)
state.putString(BrowseCataloguePresenter::query.name, query)
super.onSave(state)
}

View File

@ -1,39 +1,39 @@
package eu.kanade.tachiyomi.ui.latest_updates
import android.os.Bundle
import android.support.v4.widget.DrawerLayout
import android.view.Menu
import android.view.ViewGroup
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
/**
* Controller that shows the latest manga from the catalogue. Inherit [CatalogueController].
*/
class LatestUpdatesController(bundle: Bundle) : CatalogueController(bundle) {
constructor(source: CatalogueSource) : this(Bundle().apply {
putLong(SOURCE_ID_KEY, source.id)
})
override fun createPresenter(): CataloguePresenter {
return LatestUpdatesPresenter(args.getLong(SOURCE_ID_KEY))
}
override fun onPrepareOptionsMenu(menu: Menu) {
super.onPrepareOptionsMenu(menu)
menu.findItem(R.id.action_search).isVisible = false
menu.findItem(R.id.action_set_filter).isVisible = false
}
override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup? {
return null
}
override fun cleanupSecondaryDrawer(drawer: DrawerLayout) {
}
}
package eu.kanade.tachiyomi.ui.catalogue.latest
import android.os.Bundle
import android.support.v4.widget.DrawerLayout
import android.view.Menu
import android.view.ViewGroup
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController
import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCataloguePresenter
/**
* Controller that shows the latest manga from the catalogue. Inherit [BrowseCatalogueController].
*/
class LatestUpdatesController(bundle: Bundle) : BrowseCatalogueController(bundle) {
constructor(source: CatalogueSource) : this(Bundle().apply {
putLong(SOURCE_ID_KEY, source.id)
})
override fun createPresenter(): BrowseCataloguePresenter {
return LatestUpdatesPresenter(args.getLong(SOURCE_ID_KEY))
}
override fun onPrepareOptionsMenu(menu: Menu) {
super.onPrepareOptionsMenu(menu)
menu.findItem(R.id.action_search).isVisible = false
menu.findItem(R.id.action_set_filter).isVisible = false
}
override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup? {
return null
}
override fun cleanupSecondaryDrawer(drawer: DrawerLayout) {
}
}

View File

@ -1,8 +1,8 @@
package eu.kanade.tachiyomi.ui.latest_updates
package eu.kanade.tachiyomi.ui.catalogue.latest
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.ui.catalogue.Pager
import eu.kanade.tachiyomi.ui.catalogue.browse.Pager
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers

View File

@ -0,0 +1,16 @@
package eu.kanade.tachiyomi.ui.catalogue.latest
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCataloguePresenter
import eu.kanade.tachiyomi.ui.catalogue.browse.Pager
/**
* Presenter of [LatestUpdatesController]. Inherit BrowseCataloguePresenter.
*/
class LatestUpdatesPresenter(sourceId: Long) : BrowseCataloguePresenter(sourceId) {
override fun createPager(query: String, filters: FilterList): Pager {
return LatestUpdatesPager(source)
}
}

View File

@ -1,238 +0,0 @@
package eu.kanade.tachiyomi.ui.catalogue.main
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.SearchView
import android.view.*
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.online.LoginSource
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
import eu.kanade.tachiyomi.ui.latest_updates.LatestUpdatesController
import eu.kanade.tachiyomi.ui.setting.SettingsSourcesController
import eu.kanade.tachiyomi.widget.preference.SourceLoginDialog
import kotlinx.android.synthetic.main.catalogue_main_controller.view.*
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* This controller shows and manages the different catalogues enabled by the user.
* This controller should only handle UI actions, IO actions should be done by [CatalogueMainPresenter]
* [SourceLoginDialog.Listener] refreshes the adapter on successful login of catalogues.
* [CatalogueMainAdapter.OnBrowseClickListener] call function data on browse item click.
* [CatalogueMainAdapter.OnLatestClickListener] call function data on latest item click
*/
class CatalogueMainController : NucleusController<CatalogueMainPresenter>(),
SourceLoginDialog.Listener,
FlexibleAdapter.OnItemClickListener,
CatalogueMainAdapter.OnBrowseClickListener,
CatalogueMainAdapter.OnLatestClickListener {
/**
* Application preferences.
*/
private val preferences: PreferencesHelper = Injekt.get()
/**
* Adapter containing sources.
*/
private var adapter : CatalogueMainAdapter? = null
/**
* Called when controller is initialized.
*/
init {
// Enable the option menu
setHasOptionsMenu(true)
}
/**
* Set the title of controller.
*
* @return title.
*/
override fun getTitle(): String? {
return applicationContext?.getString(R.string.label_catalogues)
}
/**
* Create the [CatalogueMainPresenter] used in controller.
*
* @return instance of [CatalogueMainPresenter]
*/
override fun createPresenter(): CatalogueMainPresenter {
return CatalogueMainPresenter()
}
/**
* Initiate the view with [R.layout.catalogue_main_controller].
*
* @param inflater used to load the layout xml.
* @param container containing parent views.
* @return inflated view.
*/
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
return inflater.inflate(R.layout.catalogue_main_controller, container, false)
}
/**
* Called when the view is created
*
* @param view view of controller
* @param savedViewState information from previous state.
*/
override fun onViewCreated(view: View, savedViewState: Bundle?) {
super.onViewCreated(view, savedViewState)
adapter = CatalogueMainAdapter(this)
with(view) {
// Create recycler and set adapter.
recycler.layoutManager = LinearLayoutManager(context)
recycler.adapter = adapter
recycler.addItemDecoration(SourceDividerItemDecoration(context))
}
}
override fun onDestroyView(view: View) {
adapter = null
super.onDestroyView(view)
}
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
super.onChangeStarted(handler, type)
if (!type.isPush && handler is SettingsSourcesFadeChangeHandler) {
presenter.updateSources()
}
}
/**
* Called when login dialog is closed, refreshes the adapter.
*
* @param source clicked item containing source information.
*/
override fun loginDialogClosed(source: LoginSource) {
if (source.isLogged()) {
adapter?.clear()
presenter.loadSources()
}
}
/**
* Called when item is clicked
*/
override fun onItemClick(position: Int): Boolean {
val item = adapter?.getItem(position) as? SourceItem ?: return false
val source = item.source
if (source is LoginSource && !source.isLogged()) {
val dialog = SourceLoginDialog(source)
dialog.targetController = this
dialog.showDialog(router)
} else {
// Open the catalogue view.
openCatalogue(source, CatalogueController(source))
}
return false
}
/**
* Called when browse is clicked in [CatalogueMainAdapter]
*/
override fun onBrowseClick(position: Int) {
onItemClick(position)
}
/**
* Called when latest is clicked in [CatalogueMainAdapter]
*/
override fun onLatestClick(position: Int) {
val item = adapter?.getItem(position) as? SourceItem ?: return
openCatalogue(item.source, LatestUpdatesController(item.source))
}
/**
* Opens a catalogue with the given controller.
*/
private fun openCatalogue(source: CatalogueSource, controller: CatalogueController) {
preferences.lastUsedCatalogueSource().set(source.id)
router.pushController(RouterTransaction.with(controller)
.popChangeHandler(FadeChangeHandler())
.pushChangeHandler(FadeChangeHandler()))
}
/**
* Adds items to the options menu.
*
* @param menu menu containing options.
* @param inflater used to load the menu xml.
*/
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
// Inflate menu
inflater.inflate(R.menu.catalogue_main, menu)
// Initialize search option.
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
// Change hint to show global search.
searchView.queryHint = applicationContext?.getString(R.string.action_global_search_hint)
// Create query listener which opens the global search view.
searchView.queryTextChangeEvents()
.filter { it.isSubmitted }
.subscribeUntilDestroy {
val query = it.queryText().toString()
router.pushController((RouterTransaction.with(CatalogueSearchController(query)))
.popChangeHandler(FadeChangeHandler())
.pushChangeHandler(FadeChangeHandler()))
}
}
/**
* Called when an option menu item has been selected by the user.
*
* @param item The selected item.
* @return True if this event has been consumed, false if it has not.
*/
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
// Initialize option to open catalogue settings.
R.id.action_settings -> {
router.pushController((RouterTransaction.with(SettingsSourcesController()))
.popChangeHandler(SettingsSourcesFadeChangeHandler())
.pushChangeHandler(FadeChangeHandler()))
}
else -> return super.onOptionsItemSelected(item)
}
return true
}
/**
* Called to update adapter containing sources.
*/
fun setSources(sources: List<IFlexible<*>>) {
adapter?.updateDataSet(sources)
}
/**
* Called to set the last used catalogue at the top of the view.
*/
fun setLastUsedSource(item: SourceItem?) {
adapter?.removeAllScrollableHeaders()
if (item != null) {
adapter?.addScrollableHeader(item)
}
}
class SettingsSourcesFadeChangeHandler : FadeChangeHandler()
}

View File

@ -1,104 +0,0 @@
package eu.kanade.tachiyomi.ui.catalogue.main
import android.os.Bundle
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.*
import java.util.concurrent.TimeUnit
/**
* Presenter of [CatalogueMainController]
* Function calls should be done from here. UI calls should be done from the controller.
*
* @param sourceManager manages the different sources.
* @param preferences application preferences.
*/
class CatalogueMainPresenter(
val sourceManager: SourceManager = Injekt.get(),
private val preferences: PreferencesHelper = Injekt.get()
) : BasePresenter<CatalogueMainController>() {
/**
* Enabled sources.
*/
var sources = getEnabledSources()
/**
* Subscription for retrieving enabled sources.
*/
private var sourceSubscription: Subscription? = null
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
// Load enabled and last used sources
loadSources()
loadLastUsedSource()
}
/**
* Unsubscribe and create a new subscription to fetch enabled sources.
*/
fun loadSources() {
sourceSubscription?.unsubscribe()
val map = TreeMap<String, MutableList<CatalogueSource>> { d1, d2 ->
// Catalogues without a lang defined will be placed at the end
when {
d1 == "" && d2 != "" -> 1
d2 == "" && d1 != "" -> -1
else -> d1.compareTo(d2)
}
}
val byLang = sources.groupByTo(map, { it.lang })
val sourceItems = byLang.flatMap {
val langItem = LangItem(it.key)
it.value.map { source -> SourceItem(source, langItem) }
}
sourceSubscription = Observable.just(sourceItems)
.subscribeLatestCache(CatalogueMainController::setSources)
}
private fun loadLastUsedSource() {
val sharedObs = preferences.lastUsedCatalogueSource().asObservable().share()
// Emit the first item immediately but delay subsequent emissions by 500ms.
Observable.merge(
sharedObs.take(1),
sharedObs.skip(1).delay(500, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread()))
.distinctUntilChanged()
.map { (sourceManager.get(it) as? CatalogueSource)?.let { SourceItem(it) } }
.subscribeLatestCache(CatalogueMainController::setLastUsedSource)
}
fun updateSources() {
sources = getEnabledSources()
loadSources()
}
/**
* Returns a list of enabled sources ordered by language and name.
*
* @return list containing enabled sources.
*/
private fun getEnabledSources(): List<CatalogueSource> {
val languages = preferences.enabledLanguages().getOrDefault()
val hiddenCatalogues = preferences.hiddenCatalogues().getOrDefault()
return sourceManager.getCatalogueSources()
.filter { it.lang in languages }
.filterNot { it.id.toString() in hiddenCatalogues }
.sortedBy { "(${it.lang}) ${it.name}" } +
sourceManager.get(LocalSource.ID) as LocalSource
}
}

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.category
import android.os.Bundle
import android.support.design.widget.Snackbar
import android.support.v7.app.AppCompatActivity
import android.support.v7.view.ActionMode
@ -15,7 +14,7 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.util.toast
import kotlinx.android.synthetic.main.categories_controller.view.*
import kotlinx.android.synthetic.main.categories_controller.*
/**
* Controller to manage the categories for the users' library.
@ -70,22 +69,19 @@ class CategoryController : NucleusController<CategoryPresenter>(),
* Called after view inflation. Used to initialize the view.
*
* @param view The view of this controller.
* @param savedViewState The saved state of the view.
*/
override fun onViewCreated(view: View, savedViewState: Bundle?) {
super.onViewCreated(view, savedViewState)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
with(view) {
adapter = CategoryAdapter(this@CategoryController)
recycler.layoutManager = LinearLayoutManager(context)
recycler.setHasFixedSize(true)
recycler.adapter = adapter
adapter?.isHandleDragEnabled = true
adapter?.isPermanentDelete = false
adapter = CategoryAdapter(this@CategoryController)
recycler.layoutManager = LinearLayoutManager(view.context)
recycler.setHasFixedSize(true)
recycler.adapter = adapter
adapter?.isHandleDragEnabled = true
adapter?.isPermanentDelete = false
fab.clicks().subscribeUntilDestroy {
CategoryCreateDialog(this@CategoryController).showDialog(router, null)
}
fab.clicks().subscribeUntilDestroy {
CategoryCreateDialog(this@CategoryController).showDialog(router, null)
}
}
@ -95,12 +91,12 @@ class CategoryController : NucleusController<CategoryPresenter>(),
* @param view The view of this controller.
*/
override fun onDestroyView(view: View) {
super.onDestroyView(view)
// Manually call callback to delete categories if required
undoHelper?.onDeleteConfirmed(Snackbar.Callback.DISMISS_EVENT_MANUAL)
undoHelper = null
actionMode = null
adapter = null
super.onDestroyView(view)
}
/**
@ -111,9 +107,14 @@ class CategoryController : NucleusController<CategoryPresenter>(),
fun setCategories(categories: List<CategoryItem>) {
actionMode?.finish()
adapter?.updateDataSet(categories)
val selected = categories.filter { it.isSelected }
if (selected.isNotEmpty()) {
selected.forEach { onItemLongClick(categories.indexOf(it)) }
if (categories.isNotEmpty()) {
empty_view.hide()
val selected = categories.filter { it.isSelected }
if (selected.isNotEmpty()) {
selected.forEach { onItemLongClick(categories.indexOf(it)) }
}
} else {
empty_view.show(R.drawable.ic_shape_black_128dp, R.string.information_empty_category)
}
}

View File

@ -1,14 +1,10 @@
package eu.kanade.tachiyomi.ui.category
import android.graphics.Color
import android.graphics.Typeface
import android.view.View
import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.getRound
import kotlinx.android.synthetic.main.categories_item.view.*
import kotlinx.android.synthetic.main.categories_item.*
/**
* Holder used to display category items.
@ -16,16 +12,16 @@ import kotlinx.android.synthetic.main.categories_item.view.*
* @param view The view used by category items.
* @param adapter The adapter containing this holder.
*/
class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHolder(view, adapter) {
class CategoryHolder(view: View, val adapter: CategoryAdapter) : BaseFlexibleViewHolder(view, adapter) {
init {
// Create round letter image onclick to simulate long click
itemView.image.setOnClickListener {
image.setOnClickListener {
// Simulate long click on this view to enter selection mode
onLongClick(view)
}
setDragHandleView(itemView.reorder)
setDragHandleView(reorder)
}
/**
@ -35,11 +31,11 @@ class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHol
*/
fun bind(category: Category) {
// Set capitalized title.
itemView.title.text = category.name.capitalize()
title.text = category.name.capitalize()
// Update circle letter image.
itemView.post {
itemView.image.setImageDrawable(itemView.image.getRound(category.name.take(1).toUpperCase(),false))
image.setImageDrawable(image.getRound(category.name.take(1).toUpperCase(),false))
}
}

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.download
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager
import android.view.*
import eu.kanade.tachiyomi.R
@ -8,7 +7,7 @@ import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import kotlinx.android.synthetic.main.download_controller.view.*
import kotlinx.android.synthetic.main.download_controller.*
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
@ -52,21 +51,19 @@ class DownloadController : NucleusController<DownloadPresenter>() {
return resources?.getString(R.string.label_download_queue)
}
override fun onViewCreated(view: View, savedViewState: Bundle?) {
super.onViewCreated(view, savedViewState)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
// Check if download queue is empty and update information accordingly.
setInformationView()
// Initialize adapter.
adapter = DownloadAdapter()
with(view) {
recycler.adapter = adapter
recycler.adapter = adapter
// Set the layout manager for the recycler and fixed size.
recycler.layoutManager = LinearLayoutManager(context)
recycler.setHasFixedSize(true)
}
// Set the layout manager for the recycler and fixed size.
recycler.layoutManager = LinearLayoutManager(view.context)
recycler.setHasFixedSize(true)
// Suscribe to changes
DownloadService.runningRelay
@ -83,12 +80,12 @@ class DownloadController : NucleusController<DownloadPresenter>() {
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
for (subscription in progressSubscriptions.values) {
subscription.unsubscribe()
}
progressSubscriptions.clear()
adapter = null
super.onDestroyView(view)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@ -232,20 +229,18 @@ class DownloadController : NucleusController<DownloadPresenter>() {
* @return the holder of the download or null if it's not bound.
*/
private fun getHolder(download: Download): DownloadHolder? {
val recycler = view?.recycler ?: return null
return recycler.findViewHolderForItemId(download.chapter.id!!) as? DownloadHolder
return recycler?.findViewHolderForItemId(download.chapter.id!!) as? DownloadHolder
}
/**
* Set information view when queue is empty
*/
private fun setInformationView() {
val emptyView = view?.empty_view ?: return
if (presenter.downloadQueue.isEmpty()) {
emptyView.show(R.drawable.ic_file_download_black_128dp,
empty_view?.show(R.drawable.ic_file_download_black_128dp,
R.string.information_no_downloads)
} else {
emptyView.hide()
empty_view?.hide()
}
}

View File

@ -1,8 +1,8 @@
package eu.kanade.tachiyomi.ui.download
import android.support.v7.widget.RecyclerView
import android.view.View
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.ui.base.holder.BaseViewHolder
import kotlinx.android.synthetic.main.download_item.view.*
/**
@ -12,7 +12,7 @@ import kotlinx.android.synthetic.main.download_item.view.*
* @param view the inflated view for this holder.
* @constructor creates a new download holder.
*/
class DownloadHolder(private val view: View) : RecyclerView.ViewHolder(view) {
class DownloadHolder(private val view: View) : BaseViewHolder(view) {
private lateinit var download: Download

View File

@ -1,16 +0,0 @@
package eu.kanade.tachiyomi.ui.latest_updates
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
import eu.kanade.tachiyomi.ui.catalogue.Pager
/**
* Presenter of [LatestUpdatesController]. Inherit CataloguePresenter.
*/
class LatestUpdatesPresenter(sourceId: Long) : CataloguePresenter(sourceId) {
override fun createPager(query: String, filters: FilterList): Pager {
return LatestUpdatesPager(source)
}
}

View File

@ -26,6 +26,8 @@ class LibraryAdapter(private val controller: LibraryController) : RecyclerViewPa
}
}
private var boundViews = arrayListOf<View>()
/**
* Creates a new view for this adapter.
*
@ -45,6 +47,7 @@ class LibraryAdapter(private val controller: LibraryController) : RecyclerViewPa
*/
override fun bindView(view: View, position: Int) {
(view as LibraryCategoryView).onBind(categories[position])
boundViews.add(view)
}
/**
@ -55,6 +58,7 @@ class LibraryAdapter(private val controller: LibraryController) : RecyclerViewPa
*/
override fun recycleView(view: View, position: Int) {
(view as LibraryCategoryView).onRecycle()
boundViews.remove(view)
}
/**
@ -85,4 +89,15 @@ class LibraryAdapter(private val controller: LibraryController) : RecyclerViewPa
return if (index == -1) POSITION_NONE else index
}
/**
* Called when the view of this adapter is being destroyed.
*/
fun onDestroy() {
for (view in boundViews) {
if (view is LibraryCategoryView) {
view.unsubscribe()
}
}
}
}

View File

@ -124,12 +124,11 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
fun onRecycle() {
adapter.setItems(emptyList())
adapter.clearSelection()
subscriptions.clear()
unsubscribe()
}
override fun onDetachedFromWindow() {
fun unsubscribe() {
subscriptions.clear()
super.onDetachedFromWindow()
}
/**

View File

@ -14,8 +14,6 @@ import android.support.v7.widget.SearchView
import android.view.*
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import com.f2prateek.rx.preferences.Preference
import com.jakewharton.rxbinding.support.v4.view.pageSelections
import com.jakewharton.rxbinding.support.v7.widget.queryTextChanges
@ -30,13 +28,14 @@ import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.category.CategoryController
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.inflate
import eu.kanade.tachiyomi.util.toast
import eu.kanade.tachiyomi.widget.DrawerSwipeCloseListener
import kotlinx.android.synthetic.main.library_controller.view.*
import kotlinx.android.synthetic.main.library_controller.*
import kotlinx.android.synthetic.main.main_activity.*
import rx.Subscription
import timber.log.Timber
@ -100,14 +99,8 @@ class LibraryController(
private set
/**
* TabLayout of the categories.
* Adapter of the view pager.
*/
private val tabs: TabLayout?
get() = activity?.tabs
private val drawer: DrawerLayout?
get() = activity?.drawer
private var adapter: LibraryAdapter? = null
/**
@ -126,6 +119,7 @@ class LibraryController(
init {
setHasOptionsMenu(true)
retainViewMode = RetainViewMode.RETAIN_DETACH
}
override fun getTitle(): String? {
@ -140,43 +134,42 @@ class LibraryController(
return inflater.inflate(R.layout.library_controller, container, false)
}
override fun onViewCreated(view: View, savedViewState: Bundle?) {
super.onViewCreated(view, savedViewState)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
adapter = LibraryAdapter(this)
with(view) {
view_pager.adapter = adapter
view_pager.pageSelections().skip(1).subscribeUntilDestroy {
preferences.lastUsedCategory().set(it)
activeCategory = it
}
library_pager.adapter = adapter
library_pager.pageSelections().skip(1).subscribeUntilDestroy {
preferences.lastUsedCategory().set(it)
activeCategory = it
}
getColumnsPreferenceForCurrentOrientation().asObservable()
.doOnNext { mangaPerRow = it }
.skip(1)
// Set again the adapter to recalculate the covers height
.subscribeUntilDestroy { reattachAdapter() }
getColumnsPreferenceForCurrentOrientation().asObservable()
.doOnNext { mangaPerRow = it }
.skip(1)
// Set again the adapter to recalculate the covers height
.subscribeUntilDestroy { reattachAdapter() }
if (selectedMangas.isNotEmpty()) {
createActionModeIfNeeded()
}
if (selectedMangas.isNotEmpty()) {
createActionModeIfNeeded()
}
}
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
super.onChangeStarted(handler, type)
if (type.isEnter) {
activity?.tabs?.setupWithViewPager(view?.view_pager)
activity?.tabs?.setupWithViewPager(library_pager)
presenter.subscribeLibrary()
}
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
adapter?.onDestroy()
adapter = null
actionMode = null
tabsVisibilitySubscription?.unsubscribe()
tabsVisibilitySubscription = null
super.onDestroyView(view)
}
override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup {
@ -232,14 +225,14 @@ class LibraryController(
// Show empty view if needed
if (mangaMap.isNotEmpty()) {
view.empty_view.hide()
empty_view.hide()
} else {
view.empty_view.show(R.drawable.ic_book_black_128dp, R.string.information_empty_library)
empty_view.show(R.drawable.ic_book_black_128dp, R.string.information_empty_library)
}
// Get the current active category.
val activeCat = if (adapter.categories.isNotEmpty())
view.view_pager.currentItem
library_pager.currentItem
else
activeCategory
@ -247,14 +240,14 @@ class LibraryController(
adapter.categories = categories
// Restore active category.
view.view_pager.setCurrentItem(activeCat, false)
library_pager.setCurrentItem(activeCat, false)
tabsVisibilityRelay.call(categories.size > 1)
// Delay the scroll position to allow the view to be properly measured.
view.post {
if (isAttached) {
tabs?.setScrollPosition(view.view_pager.currentItem, 0f, true)
activity?.tabs?.setScrollPosition(library_pager.currentItem, 0f, true)
}
}
@ -297,14 +290,13 @@ class LibraryController(
* Reattaches the adapter to the view pager to recreate fragments
*/
private fun reattachAdapter() {
val pager = view?.view_pager ?: return
val adapter = adapter ?: return
val position = pager.currentItem
val position = library_pager.currentItem
adapter.recycle = false
pager.adapter = adapter
pager.currentItem = position
library_pager.adapter = adapter
library_pager.currentItem = position
adapter.recycle = true
}
@ -330,7 +322,7 @@ class LibraryController(
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
if (!query.isNullOrEmpty()) {
if (!query.isEmpty()) {
searchItem.expandActionView()
searchView.setQuery(query, true)
searchView.clearFocus()
@ -360,15 +352,13 @@ class LibraryController(
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_filter -> {
navView?.let { drawer?.openDrawer(Gravity.END) }
navView?.let { activity?.drawer?.openDrawer(Gravity.END) }
}
R.id.action_update_library -> {
activity?.let { LibraryUpdateService.start(it) }
}
R.id.action_edit_categories -> {
router.pushController(RouterTransaction.with(CategoryController())
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler()))
router.pushController(CategoryController().withFadeTransaction())
}
else -> return super.onOptionsItemSelected(item)
}
@ -424,9 +414,7 @@ class LibraryController(
// Notify the presenter a manga is being opened.
presenter.onOpenManga()
router.pushController(RouterTransaction.with(MangaController(manga))
.pushChangeHandler(FadeChangeHandler(false))
.popChangeHandler(FadeChangeHandler()))
router.pushController(MangaController(manga).withFadeTransaction())
}
/**
@ -461,11 +449,11 @@ class LibraryController(
.toTypedArray()
ChangeMangaCategoriesDialog(this, mangas, categories, commonCategoriesIndexes)
.showDialog(router, null)
.showDialog(router)
}
private fun showDeleteMangaDialog() {
DeleteLibraryMangasDialog(this, selectedMangas.toList()).showDialog(router, null)
DeleteLibraryMangasDialog(this, selectedMangas.toList()).showDialog(router)
}
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
@ -480,8 +468,6 @@ class LibraryController(
/**
* Changes the cover for the selected manga.
*
* @param mangas a list of selected manga.
*/
private fun changeSelectedCover() {
val manga = selectedMangas.firstOrNull() ?: return

View File

@ -5,7 +5,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.source.LocalSource
import kotlinx.android.synthetic.main.catalogue_grid_item.view.*
import kotlinx.android.synthetic.main.catalogue_grid_item.*
/**
* Class used to hold the displayed data of a manga in the library, like the cover or the title.
@ -30,30 +30,28 @@ class LibraryGridHolder(
*/
override fun onSetValues(item: LibraryItem) {
// Update the title of the manga.
view.title.text = item.manga.title
title.text = item.manga.title
// Update the unread count and its visibility.
with(view.unread_text) {
with(unread_text) {
visibility = if (item.manga.unread > 0) View.VISIBLE else View.GONE
text = item.manga.unread.toString()
}
// Update the download count and its visibility.
with(view.download_text) {
with(download_text) {
visibility = if (item.downloadCount > 0) View.VISIBLE else View.GONE
text = item.downloadCount.toString()
}
//set local visibility if its local manga
with(view.local_text) {
visibility = if(item.manga.source == LocalSource.ID) View.VISIBLE else View.GONE
}
local_text.visibility = if(item.manga.source == LocalSource.ID) View.VISIBLE else View.GONE
// Update the cover.
GlideApp.with(view.context).clear(view.thumbnail)
GlideApp.with(view.context).clear(thumbnail)
GlideApp.with(view.context)
.load(item.manga)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop()
.into(view.thumbnail)
.into(thumbnail)
}
}

View File

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.library
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
/**
* Generic class used to hold the displayed data of a manga in the library.
@ -14,7 +14,7 @@ import eu.davidea.viewholders.FlexibleViewHolder
abstract class LibraryHolder(
view: View,
adapter: FlexibleAdapter<*>
) : FlexibleViewHolder(view, adapter) {
) : BaseFlexibleViewHolder(view, adapter) {
/**
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this

View File

@ -5,7 +5,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.source.LocalSource
import kotlinx.android.synthetic.main.catalogue_list_item.view.*
import kotlinx.android.synthetic.main.catalogue_list_item.*
/**
* Class used to hold the displayed data of a manga in the library, like the cover or the title.
@ -30,38 +30,36 @@ class LibraryListHolder(
*/
override fun onSetValues(item: LibraryItem) {
// Update the title of the manga.
itemView.title.text = item.manga.title
title.text = item.manga.title
// Update the unread count and its visibility.
with(itemView.unread_text) {
with(unread_text) {
visibility = if (item.manga.unread > 0) View.VISIBLE else View.GONE
text = item.manga.unread.toString()
}
// Update the download count and its visibility.
with(itemView.download_text) {
with(download_text) {
visibility = if (item.downloadCount > 0) View.VISIBLE else View.GONE
text = "${item.downloadCount}"
}
//show local text badge if local manga
with(itemView.local_text) {
visibility = if (item.manga.source == LocalSource.ID) View.VISIBLE else View.GONE
}
local_text.visibility = if (item.manga.source == LocalSource.ID) View.VISIBLE else View.GONE
// Create thumbnail onclick to simulate long click
itemView.thumbnail.setOnClickListener {
thumbnail.setOnClickListener {
// Simulate long click on this view to enter selection mode
onLongClick(itemView)
}
// Update the cover.
GlideApp.with(itemView.context).clear(itemView.thumbnail)
GlideApp.with(itemView.context).clear(thumbnail)
GlideApp.with(itemView.context)
.load(item.manga)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop()
.circleCrop()
.dontAnimate()
.into(itemView.thumbnail)
.into(thumbnail)
}
}

View File

@ -9,16 +9,12 @@ import android.support.v4.widget.DrawerLayout
import android.support.v7.graphics.drawable.DrawerArrowDrawable
import android.view.ViewGroup
import com.bluelinelabs.conductor.*
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import eu.kanade.tachiyomi.Migrations
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
import eu.kanade.tachiyomi.ui.catalogue.main.CatalogueMainController
import eu.kanade.tachiyomi.ui.base.controller.*
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
import eu.kanade.tachiyomi.ui.download.DownloadController
import eu.kanade.tachiyomi.ui.library.LibraryController
import eu.kanade.tachiyomi.ui.manga.MangaController
@ -83,16 +79,13 @@ class MainActivity : BaseActivity() {
R.id.nav_drawer_library -> setRoot(LibraryController(), id)
R.id.nav_drawer_recent_updates -> setRoot(RecentChaptersController(), id)
R.id.nav_drawer_recently_read -> setRoot(RecentlyReadController(), id)
R.id.nav_drawer_catalogues -> setRoot(CatalogueMainController(), id)
R.id.nav_drawer_catalogues -> setRoot(CatalogueController(), id)
R.id.nav_drawer_downloads -> {
router.pushController(RouterTransaction.with(DownloadController())
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler()))
router.pushController(DownloadController().withFadeTransaction())
}
R.id.nav_drawer_settings -> {
router.pushController(SettingsMainController().withFadeTransaction())
}
R.id.nav_drawer_settings ->
router.pushController(RouterTransaction.with(SettingsMainController())
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler()))
}
}
drawer.closeDrawer(GravityCompat.START)
@ -189,10 +182,7 @@ class MainActivity : BaseActivity() {
}
private fun setRoot(controller: Controller, id: Int) {
router.setRoot(RouterTransaction.with(controller)
.popChangeHandler(FadeChangeHandler())
.pushChangeHandler(FadeChangeHandler())
.tag(id.toString()))
router.setRoot(controller.withFadeTransaction().tag(id.toString()))
}
private fun syncActivityViewWithController(to: Controller?, from: Controller? = null) {

View File

@ -1,8 +1,6 @@
package eu.kanade.tachiyomi.ui.manga
import android.Manifest.permission.READ_EXTERNAL_STORAGE
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.os.Build
import android.os.Bundle
import android.support.design.widget.TabLayout
import android.support.graphics.drawable.VectorDrawableCompat
@ -32,7 +30,7 @@ import eu.kanade.tachiyomi.ui.manga.info.MangaInfoController
import eu.kanade.tachiyomi.ui.manga.track.TrackController
import eu.kanade.tachiyomi.util.toast
import kotlinx.android.synthetic.main.main_activity.*
import kotlinx.android.synthetic.main.manga_controller.view.*
import kotlinx.android.synthetic.main.manga_controller.*
import rx.Subscription
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -81,21 +79,19 @@ class MangaController : RxController, TabbedController {
return inflater.inflate(R.layout.manga_controller, container, false)
}
override fun onViewCreated(view: View, savedViewState: Bundle?) {
super.onViewCreated(view, savedViewState)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
if (manga == null || source == null) return
requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301)
with(view) {
adapter = MangaDetailAdapter()
view_pager.offscreenPageLimit = 3
view_pager.adapter = adapter
adapter = MangaDetailAdapter()
manga_pager.offscreenPageLimit = 3
manga_pager.adapter = adapter
if (!fromCatalogue)
view_pager.currentItem = CHAPTERS_CONTROLLER
}
if (!fromCatalogue)
manga_pager.currentItem = CHAPTERS_CONTROLLER
}
override fun onDestroyView(view: View) {
@ -106,7 +102,7 @@ class MangaController : RxController, TabbedController {
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
super.onChangeStarted(handler, type)
if (type.isEnter) {
activity?.tabs?.setupWithViewPager(view?.view_pager)
activity?.tabs?.setupWithViewPager(manga_pager)
trackingIconSubscription = trackingIconRelay.subscribe { setTrackingIconInternal(it) }
}
}

View File

@ -2,41 +2,41 @@ package eu.kanade.tachiyomi.ui.manga.chapter
import android.view.View
import android.widget.PopupMenu
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.getResourceColor
import eu.kanade.tachiyomi.util.gone
import eu.kanade.tachiyomi.util.setVectorCompat
import kotlinx.android.synthetic.main.chapters_item.view.*
import kotlinx.android.synthetic.main.chapters_item.*
import java.util.*
class ChapterHolder(
private val view: View,
private val adapter: ChaptersAdapter
) : FlexibleViewHolder(view, adapter) {
) : BaseFlexibleViewHolder(view, adapter) {
init {
// We need to post a Runnable to show the popup to make sure that the PopupMenu is
// correctly positioned. The reason being that the view may change position before the
// PopupMenu is shown.
view.chapter_menu.setOnClickListener { it.post { showPopupMenu(it) } }
chapter_menu.setOnClickListener { it.post { showPopupMenu(it) } }
}
fun bind(item: ChapterItem, manga: Manga) = with(view) {
fun bind(item: ChapterItem, manga: Manga) {
val chapter = item.chapter
chapter_title.text = when (manga.displayMode) {
Manga.DISPLAY_NUMBER -> {
val number = adapter.decimalFormat.format(chapter.chapter_number.toDouble())
context.getString(R.string.display_mode_chapter, number)
itemView.context.getString(R.string.display_mode_chapter, number)
}
else -> chapter.name
}
// Set the correct drawable for dropdown and update the tint to match theme.
view.chapter_menu.setVectorCompat(R.drawable.ic_more_horiz_black_24dp, view.context.getResourceColor(R.attr.icon_color))
chapter_menu.setVectorCompat(R.drawable.ic_more_horiz_black_24dp, view.context.getResourceColor(R.attr.icon_color))
// Set correct text color
chapter_title.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor)
@ -53,14 +53,14 @@ class ChapterHolder(
chapter_scanlator.text = chapter.scanlator
//allow longer titles if there is no scanlator (most sources)
if (chapter_scanlator.text.isNullOrBlank()) {
chapter_title.setMaxLines(2)
chapter_title.maxLines = 2
chapter_scanlator.gone()
} else {
chapter_title.setMaxLines(1)
chapter_title.maxLines = 1
}
chapter_pages.text = if (!chapter.read && chapter.last_page_read > 0) {
context.getString(R.string.chapter_progress, chapter.last_page_read + 1)
itemView.context.getString(R.string.chapter_progress, chapter.last_page_read + 1)
} else {
""
}
@ -68,7 +68,7 @@ class ChapterHolder(
notifyStatus(item.status)
}
fun notifyStatus(status: Int) = with(view.download_text) {
fun notifyStatus(status: Int) = with(download_text) {
when (status) {
Download.QUEUE -> setText(R.string.chapter_queued)
Download.DOWNLOADING -> setText(R.string.chapter_downloading)

View File

@ -4,7 +4,6 @@ import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.support.design.widget.Snackbar
import android.support.v7.app.AppCompatActivity
import android.support.v7.view.ActionMode
@ -26,7 +25,7 @@ import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.getCoordinates
import eu.kanade.tachiyomi.util.snack
import eu.kanade.tachiyomi.util.toast
import kotlinx.android.synthetic.main.chapters_controller.view.*
import kotlinx.android.synthetic.main.chapters_controller.*
import timber.log.Timber
class ChaptersController : NucleusController<ChaptersPresenter>(),
@ -69,57 +68,55 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
return inflater.inflate(R.layout.chapters_controller, container, false)
}
override fun onViewCreated(view: View, savedViewState: Bundle?) {
super.onViewCreated(view, savedViewState)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
// Init RecyclerView and adapter
adapter = ChaptersAdapter(this, view.context)
with(view) {
recycler.adapter = adapter
recycler.layoutManager = LinearLayoutManager(context)
recycler.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
recycler.setHasFixedSize(true)
adapter?.fastScroller = view.fast_scroller
recycler.adapter = adapter
recycler.layoutManager = LinearLayoutManager(view.context)
recycler.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
recycler.setHasFixedSize(true)
adapter?.fastScroller = fast_scroller
swipe_refresh.refreshes().subscribeUntilDestroy { fetchChaptersFromSource() }
swipe_refresh.refreshes().subscribeUntilDestroy { fetchChaptersFromSource() }
fab.clicks().subscribeUntilDestroy {
val item = presenter.getNextUnreadChapter()
if (item != null) {
// Create animation listener
val revealAnimationListener: Animator.AnimatorListener = object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator?) {
openChapter(item.chapter, true)
}
fab.clicks().subscribeUntilDestroy {
val item = presenter.getNextUnreadChapter()
if (item != null) {
// Create animation listener
val revealAnimationListener: Animator.AnimatorListener = object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator?) {
openChapter(item.chapter, true)
}
// Get coordinates and start animation
val coordinates = fab.getCoordinates()
if (!reveal_view.showRevealEffect(coordinates.x, coordinates.y, revealAnimationListener)) {
openChapter(item.chapter)
}
} else {
context.toast(R.string.no_next_chapter)
}
// Get coordinates and start animation
val coordinates = fab.getCoordinates()
if (!reveal_view.showRevealEffect(coordinates.x, coordinates.y, revealAnimationListener)) {
openChapter(item.chapter)
}
} else {
view.context.toast(R.string.no_next_chapter)
}
}
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
adapter = null
actionMode = null
super.onDestroyView(view)
}
override fun onActivityResumed(activity: Activity) {
val view = view ?: return
if (view == null) return
// Check if animation view is visible
if (view.reveal_view.visibility == View.VISIBLE) {
if (reveal_view.visibility == View.VISIBLE) {
// Show the unReveal effect
val coordinates = view.fab.getCoordinates()
view.reveal_view.hideRevealEffect(coordinates.x, coordinates.y, 1920)
val coordinates = fab.getCoordinates()
reveal_view.hideRevealEffect(coordinates.x, coordinates.y, 1920)
}
super.onActivityResumed(activity)
}
@ -213,16 +210,16 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
}
fun fetchChaptersFromSource() {
view?.swipe_refresh?.isRefreshing = true
swipe_refresh?.isRefreshing = true
presenter.fetchChaptersFromSource()
}
fun onFetchChaptersDone() {
view?.swipe_refresh?.isRefreshing = false
swipe_refresh?.isRefreshing = false
}
fun onFetchChaptersError(error: Throwable) {
view?.swipe_refresh?.isRefreshing = false
swipe_refresh?.isRefreshing = false
activity?.toast(error.message)
}
@ -231,7 +228,7 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
}
private fun getHolder(chapter: Chapter): ChapterHolder? {
return view?.recycler?.findViewHolderForItemId(chapter.id!!) as? ChapterHolder
return recycler?.findViewHolderForItemId(chapter.id!!) as? ChapterHolder
}
fun openChapter(chapter: Chapter, hasAnimation: Boolean = false) {
@ -365,7 +362,7 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
destroyActionModeIfNeeded()
presenter.downloadChapters(chapters)
if (view != null && !presenter.manga.favorite) {
view.recycler?.snack(view.context.getString(R.string.snack_add_to_library), Snackbar.LENGTH_INDEFINITE) {
recycler?.snack(view.context.getString(R.string.snack_add_to_library), Snackbar.LENGTH_INDEFINITE) {
setAction(R.string.action_add) {
presenter.addToLibrary()
}

View File

@ -39,7 +39,7 @@ import eu.kanade.tachiyomi.util.snack
import eu.kanade.tachiyomi.util.toast
import jp.wasabeef.glide.transformations.CropSquareTransformation
import jp.wasabeef.glide.transformations.MaskTransformation
import kotlinx.android.synthetic.main.manga_info_controller.view.*
import kotlinx.android.synthetic.main.manga_info_controller.*
import uy.kohesive.injekt.injectLazy
import java.text.DecimalFormat
@ -71,17 +71,14 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
return inflater.inflate(R.layout.manga_info_controller, container, false)
}
override fun onViewCreated(view: View, savedViewState: Bundle?) {
super.onViewCreated(view, savedViewState)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
with(view) {
// Set onclickListener to toggle favorite when FAB clicked.
fab_favorite.clicks().subscribeUntilDestroy { onFabClick() }
// Set SwipeRefresh to refresh manga data.
swipe_refresh.refreshes().subscribeUntilDestroy { fetchMangaFromSource() }
}
// Set onclickListener to toggle favorite when FAB clicked.
fab_favorite.clicks().subscribeUntilDestroy { onFabClick() }
// Set SwipeRefresh to refresh manga data.
swipe_refresh.refreshes().subscribeUntilDestroy { fetchMangaFromSource() }
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@ -124,50 +121,49 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
*/
private fun setMangaInfo(manga: Manga, source: Source?) {
val view = view ?: return
with(view) {
// Update artist TextView.
manga_artist.text = manga.artist
// Update author TextView.
manga_author.text = manga.author
// Update artist TextView.
manga_artist.text = manga.artist
// If manga source is known update source TextView.
if (source != null) {
manga_source.text = source.toString()
}
// Update author TextView.
manga_author.text = manga.author
// Update genres TextView.
manga_genres.text = manga.genre
// If manga source is known update source TextView.
if (source != null) {
manga_source.text = source.toString()
}
// Update status TextView.
manga_status.setText(when (manga.status) {
SManga.ONGOING -> R.string.ongoing
SManga.COMPLETED -> R.string.completed
SManga.LICENSED -> R.string.licensed
else -> R.string.unknown
})
// Update genres TextView.
manga_genres.text = manga.genre
// Update description TextView.
manga_summary.text = manga.description
// Update status TextView.
manga_status.setText(when (manga.status) {
SManga.ONGOING -> R.string.ongoing
SManga.COMPLETED -> R.string.completed
SManga.LICENSED -> R.string.licensed
else -> R.string.unknown
})
// Set the favorite drawable to the correct one.
setFavoriteDrawable(manga.favorite)
// Update description TextView.
manga_summary.text = manga.description
// Set cover if it wasn't already.
if (manga_cover.drawable == null && !manga.thumbnail_url.isNullOrEmpty()) {
GlideApp.with(context)
// Set the favorite drawable to the correct one.
setFavoriteDrawable(manga.favorite)
// Set cover if it wasn't already.
if (manga_cover.drawable == null && !manga.thumbnail_url.isNullOrEmpty()) {
GlideApp.with(view.context)
.load(manga)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop()
.into(manga_cover)
if (backdrop != null) {
GlideApp.with(view.context)
.load(manga)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop()
.into(manga_cover)
if (backdrop != null) {
GlideApp.with(context)
.load(manga)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop()
.into(backdrop)
}
.into(backdrop)
}
}
}
@ -178,7 +174,7 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
* @param count number of chapters.
*/
fun setChapterCount(count: Float) {
view?.manga_chapters?.text = DecimalFormat("#.#").format(count)
manga_chapters?.text = DecimalFormat("#.#").format(count)
}
/**
@ -243,10 +239,10 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
private fun setFavoriteDrawable(isFavorite: Boolean) {
// Set the Favorite drawable to the correct one.
// Border drawable if false, filled drawable if true.
view?.fab_favorite?.setImageResource(if (isFavorite)
fab_favorite?.setImageResource(if (isFavorite)
R.drawable.ic_bookmark_white_24dp
else
R.drawable.ic_bookmark_border_white_24dp)
R.drawable.ic_add_to_library_24dp)
}
/**
@ -279,7 +275,7 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
* @param value whether it should be refreshing or not.
*/
private fun setRefreshing(value: Boolean) {
view?.swipe_refresh?.isRefreshing = value
swipe_refresh?.isRefreshing = value
}
/**
@ -305,6 +301,9 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
.showDialog(router)
}
}
activity?.toast(activity?.getString(R.string.manga_added_library))
}else{
activity?.toast(activity?.getString(R.string.manga_removed_library))
}
}

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.manga.track
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager
import android.view.LayoutInflater
import android.view.View
@ -11,7 +10,7 @@ import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.toast
import kotlinx.android.synthetic.main.track_controller.view.*
import kotlinx.android.synthetic.main.track_controller.*
class TrackController : NucleusController<TrackPresenter>(),
TrackAdapter.OnRowClickListener,
@ -35,8 +34,8 @@ class TrackController : NucleusController<TrackPresenter>(),
return inflater.inflate(R.layout.track_controller, container, false)
}
override fun onViewCreated(view: View, savedViewState: Bundle?) {
super.onViewCreated(view, savedViewState)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
adapter = TrackAdapter(this)
with(view) {
@ -48,14 +47,14 @@ class TrackController : NucleusController<TrackPresenter>(),
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
adapter = null
super.onDestroyView(view)
}
fun onNextTrackings(trackings: List<TrackItem>) {
val atLeastOneLink = trackings.any { it.track != null }
adapter?.items = trackings
view?.swipe_refresh?.isEnabled = atLeastOneLink
swipe_refresh?.isEnabled = atLeastOneLink
(parentController as? MangaController)?.setTrackingIcon(atLeastOneLink)
}
@ -73,11 +72,11 @@ class TrackController : NucleusController<TrackPresenter>(),
}
fun onRefreshDone() {
view?.swipe_refresh?.isRefreshing = false
swipe_refresh?.isRefreshing = false
}
fun onRefreshError(error: Throwable) {
view?.swipe_refresh?.isRefreshing = false
swipe_refresh?.isRefreshing = false
activity?.toast(error.message)
}
@ -109,17 +108,17 @@ class TrackController : NucleusController<TrackPresenter>(),
override fun setStatus(item: TrackItem, selection: Int) {
presenter.setStatus(item, selection)
view?.swipe_refresh?.isRefreshing = true
swipe_refresh?.isRefreshing = true
}
override fun setScore(item: TrackItem, score: Int) {
presenter.setScore(item, score)
view?.swipe_refresh?.isRefreshing = true
swipe_refresh?.isRefreshing = true
}
override fun setChaptersRead(item: TrackItem, chaptersRead: Int) {
presenter.setLastChapterRead(item, chaptersRead)
view?.swipe_refresh?.isRefreshing = true
swipe_refresh?.isRefreshing = true
}
private companion object {

View File

@ -1,29 +1,29 @@
package eu.kanade.tachiyomi.ui.manga.track
import android.annotation.SuppressLint
import android.support.v7.widget.RecyclerView
import android.view.View
import eu.kanade.tachiyomi.R
import kotlinx.android.synthetic.main.track_item.view.*
import eu.kanade.tachiyomi.ui.base.holder.BaseViewHolder
import kotlinx.android.synthetic.main.track_item.*
class TrackHolder(view: View, adapter: TrackAdapter) : RecyclerView.ViewHolder(view) {
class TrackHolder(view: View, adapter: TrackAdapter) : BaseViewHolder(view) {
init {
val listener = adapter.rowClickListener
view.title_container.setOnClickListener { listener.onTitleClick(adapterPosition) }
view.status_container.setOnClickListener { listener.onStatusClick(adapterPosition) }
view.chapters_container.setOnClickListener { listener.onChaptersClick(adapterPosition) }
view.score_container.setOnClickListener { listener.onScoreClick(adapterPosition) }
title_container.setOnClickListener { listener.onTitleClick(adapterPosition) }
status_container.setOnClickListener { listener.onStatusClick(adapterPosition) }
chapters_container.setOnClickListener { listener.onChaptersClick(adapterPosition) }
score_container.setOnClickListener { listener.onScoreClick(adapterPosition) }
}
@SuppressLint("SetTextI18n")
@Suppress("DEPRECATION")
fun bind(item: TrackItem) = with(itemView) {
fun bind(item: TrackItem) {
val track = item.track
track_logo.setImageResource(item.service.getLogo())
logo.setBackgroundColor(item.service.getLogoColor())
if (track != null) {
track_title.setTextAppearance(context, R.style.TextAppearance_Regular_Body1_Secondary)
track_title.setTextAppearance(itemView.context, R.style.TextAppearance_Regular_Body1_Secondary)
track_title.setAllCaps(false)
track_title.text = track.title
track_chapters.text = "${track.last_chapter_read}/" +
@ -31,7 +31,7 @@ class TrackHolder(view: View, adapter: TrackAdapter) : RecyclerView.ViewHolder(v
track_status.text = item.service.getStatus(track.status)
track_score.text = if (track.score == 0f) "-" else item.service.displayScore(track)
} else {
track_title.setTextAppearance(context, R.style.TextAppearance_Medium_Button)
track_title.setTextAppearance(itemView.context, R.style.TextAppearance_Medium_Button)
track_title.setText(R.string.action_edit)
track_chapters.text = ""
track_score.text = ""

View File

@ -118,7 +118,7 @@ abstract class PagerReader : BaseReader() {
this.pager = pager.apply {
setLayoutParams(ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT))
setOffscreenPageLimit(1)
setId(R.id.view_pager)
setId(R.id.reader_pager)
setOnChapterBoundariesOutListener(object : OnChapterBoundariesOutListener {
override fun onFirstPageOutEvent() {
readerActivity.requestPreviousChapter()

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
import android.support.v7.widget.RecyclerView
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
@ -11,10 +10,11 @@ import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.base.holder.BaseViewHolder
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.ui.reader.viewer.base.PageDecodeErrorLayout
import eu.kanade.tachiyomi.util.inflate
import kotlinx.android.synthetic.main.reader_webtoon_item.view.*
import kotlinx.android.synthetic.main.reader_webtoon_item.*
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
@ -31,7 +31,7 @@ import java.util.concurrent.TimeUnit
* @constructor creates a new webtoon holder.
*/
class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter) :
RecyclerView.ViewHolder(view) {
BaseViewHolder(view) {
/**
* Page of a chapter.
@ -54,7 +54,7 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
private var decodeErrorLayout: View? = null
init {
with(view.image_view) {
with(image_view) {
setMaxTileSize(readerActivity.maxBitmapSize)
setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED)
setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE)
@ -78,11 +78,11 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
})
}
view.progress_container.layoutParams = FrameLayout.LayoutParams(
progress_container.layoutParams = FrameLayout.LayoutParams(
MATCH_PARENT, webtoonReader.screenHeight)
view.setOnTouchListener(adapter.touchListener)
view.retry_button.setOnTouchListener { _, event ->
retry_button.setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_UP) {
readerActivity.presenter.retryPage(page)
}
@ -111,9 +111,9 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
(view as ViewGroup).removeView(it)
decodeErrorLayout = null
}
view.image_view.recycle()
view.image_view.visibility = View.GONE
view.progress_container.visibility = View.VISIBLE
image_view.recycle()
image_view.visibility = View.GONE
progress_container.visibility = View.VISIBLE
}
/**
@ -150,7 +150,7 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
.onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { progress ->
view.progress_text.text = if (progress > 0) {
progress_text.text = if (progress > 0) {
view.context.getString(R.string.download_progress, progress)
} else {
view.context.getString(R.string.downloading)
@ -279,14 +279,14 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
* Called when the image is decoded and going to be displayed.
*/
private fun onImageDecoded() {
view.progress_container.visibility = View.GONE
progress_container.visibility = View.GONE
}
/**
* Called when the image fails to decode.
*/
private fun onImageDecodeError() {
view.progress_container.visibility = View.GONE
progress_container.visibility = View.GONE
val page = page ?: return
if (decodeErrorLayout != null || !webtoonReader.isAdded) return

View File

@ -3,13 +3,13 @@ package eu.kanade.tachiyomi.ui.recent_updates
import android.view.View
import android.widget.PopupMenu
import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.getResourceColor
import eu.kanade.tachiyomi.util.setVectorCompat
import kotlinx.android.synthetic.main.recent_chapters_item.view.*
import kotlinx.android.synthetic.main.recent_chapters_item.*
/**
* Holder that contains chapter item
@ -22,7 +22,7 @@ import kotlinx.android.synthetic.main.recent_chapters_item.view.*
* @constructor creates a new recent chapter holder.
*/
class RecentChapterHolder(private val view: View, private val adapter: RecentChaptersAdapter) :
FlexibleViewHolder(view, adapter) {
BaseFlexibleViewHolder(view, adapter) {
/**
* Color of read chapter
@ -43,8 +43,8 @@ class RecentChapterHolder(private val view: View, private val adapter: RecentCha
// We need to post a Runnable to show the popup to make sure that the PopupMenu is
// correctly positioned. The reason being that the view may change position before the
// PopupMenu is shown.
view.chapter_menu.setOnClickListener { it.post { showPopupMenu(it) } }
view.manga_cover.setOnClickListener {
chapter_menu.setOnClickListener { it.post { showPopupMenu(it) } }
manga_cover.setOnClickListener {
adapter.coverClickListener.onCoverClick(adapterPosition)
}
}
@ -58,31 +58,31 @@ class RecentChapterHolder(private val view: View, private val adapter: RecentCha
this.item = item
// Set chapter title
view.chapter_title.text = item.chapter.name
chapter_title.text = item.chapter.name
// Set manga title
view.manga_title.text = item.manga.title
manga_title.text = item.manga.title
// Set the correct drawable for dropdown and update the tint to match theme.
view.chapter_menu_icon.setVectorCompat(R.drawable.ic_more_horiz_black_24dp, view.context.getResourceColor(R.attr.icon_color))
chapter_menu_icon.setVectorCompat(R.drawable.ic_more_horiz_black_24dp, view.context.getResourceColor(R.attr.icon_color))
// Set cover
GlideApp.with(itemView.context).clear(itemView.manga_cover)
GlideApp.with(itemView.context).clear(manga_cover)
if (!item.manga.thumbnail_url.isNullOrEmpty()) {
GlideApp.with(itemView.context)
.load(item.manga)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.circleCrop()
.into(itemView.manga_cover)
.into(manga_cover)
}
// Check if chapter is read and set correct color
if (item.chapter.read) {
view.chapter_title.setTextColor(readColor)
view.manga_title.setTextColor(readColor)
chapter_title.setTextColor(readColor)
manga_title.setTextColor(readColor)
} else {
view.chapter_title.setTextColor(unreadColor)
view.manga_title.setTextColor(unreadColor)
chapter_title.setTextColor(unreadColor)
manga_title.setTextColor(unreadColor)
}
// Set chapter status
@ -94,7 +94,7 @@ class RecentChapterHolder(private val view: View, private val adapter: RecentCha
*
* @param status download status
*/
fun notifyStatus(status: Int) = with(view.download_text) {
fun notifyStatus(status: Int) = with(download_text) {
when (status) {
Download.QUEUE -> setText(R.string.chapter_queued)
Download.DOWNLOADING -> setText(R.string.chapter_downloading)

View File

@ -1,13 +1,10 @@
package eu.kanade.tachiyomi.ui.recent_updates
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.support.v7.view.ActionMode
import android.support.v7.widget.DividerItemDecoration
import android.support.v7.widget.LinearLayoutManager
import android.view.*
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import com.jakewharton.rxbinding.support.v4.widget.refreshes
import com.jakewharton.rxbinding.support.v7.widget.scrollStateChanges
import eu.davidea.flexibleadapter.FlexibleAdapter
@ -19,10 +16,11 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateService
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.toast
import kotlinx.android.synthetic.main.recent_chapters_controller.view.*
import kotlinx.android.synthetic.main.recent_chapters_controller.*
import timber.log.Timber
/**
@ -65,42 +63,39 @@ class RecentChaptersController : NucleusController<RecentChaptersPresenter>(),
/**
* Called when view is created
* @param view created view
* @param savedViewState status of saved sate
*/
override fun onViewCreated(view: View, savedViewState: Bundle?) {
super.onViewCreated(view, savedViewState)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
with(view) {
// Init RecyclerView and adapter
val layoutManager = LinearLayoutManager(context)
recycler.layoutManager = layoutManager
recycler.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
recycler.setHasFixedSize(true)
adapter = RecentChaptersAdapter(this@RecentChaptersController)
recycler.adapter = adapter
// Init RecyclerView and adapter
val layoutManager = LinearLayoutManager(view.context)
recycler.layoutManager = layoutManager
recycler.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
recycler.setHasFixedSize(true)
adapter = RecentChaptersAdapter(this@RecentChaptersController)
recycler.adapter = adapter
recycler.scrollStateChanges().subscribeUntilDestroy {
// Disable swipe refresh when view is not at the top
val firstPos = layoutManager.findFirstCompletelyVisibleItemPosition()
swipe_refresh.isEnabled = firstPos <= 0
}
swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt())
swipe_refresh.refreshes().subscribeUntilDestroy {
if (!LibraryUpdateService.isRunning(context)) {
LibraryUpdateService.start(context)
context.toast(R.string.action_update_library)
}
// It can be a very long operation, so we disable swipe refresh and show a toast.
swipe_refresh.isRefreshing = false
recycler.scrollStateChanges().subscribeUntilDestroy {
// Disable swipe refresh when view is not at the top
val firstPos = layoutManager.findFirstCompletelyVisibleItemPosition()
swipe_refresh.isEnabled = firstPos <= 0
}
swipe_refresh.setDistanceToTriggerSync((2 * 64 * view.resources.displayMetrics.density).toInt())
swipe_refresh.refreshes().subscribeUntilDestroy {
if (!LibraryUpdateService.isRunning(view.context)) {
LibraryUpdateService.start(view.context)
view.context.toast(R.string.action_update_library)
}
// It can be a very long operation, so we disable swipe refresh and show a toast.
swipe_refresh.isRefreshing = false
}
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
adapter = null
actionMode = null
super.onDestroyView(view)
}
/**
@ -180,11 +175,10 @@ class RecentChaptersController : NucleusController<RecentChaptersPresenter>(),
}
override fun onUpdateEmptyView(size: Int) {
val emptyView = view?.empty_view ?: return
if (size > 0) {
emptyView.hide()
empty_view?.hide()
} else {
emptyView.show(R.drawable.ic_update_black_128dp, R.string.information_no_recent)
empty_view?.show(R.drawable.ic_update_black_128dp, R.string.information_no_recent)
}
}
@ -201,7 +195,7 @@ class RecentChaptersController : NucleusController<RecentChaptersPresenter>(),
* @param download [Download] object containing download progress.
*/
private fun getHolder(download: Download): RecentChapterHolder? {
return view?.recycler?.findViewHolderForItemId(download.chapter.id!!) as? RecentChapterHolder
return recycler?.findViewHolderForItemId(download.chapter.id!!) as? RecentChapterHolder
}
/**
@ -260,9 +254,7 @@ class RecentChaptersController : NucleusController<RecentChaptersPresenter>(),
}
fun openManga(chapter: RecentChapterItem) {
router.pushController(RouterTransaction.with(MangaController(chapter.manga))
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler()))
router.pushController(MangaController(chapter.manga).withFadeTransaction())
}
/**

View File

@ -1,21 +1,19 @@
package eu.kanade.tachiyomi.ui.recently_read
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.toast
import kotlinx.android.synthetic.main.recently_read_controller.view.*
import kotlinx.android.synthetic.main.recently_read_controller.*
/**
* Fragment that shows recently read manga.
@ -51,23 +49,20 @@ class RecentlyReadController : NucleusController<RecentlyReadPresenter>(),
* Called when view is created
*
* @param view created view
* @param savedViewState saved state of the view
*/
override fun onViewCreated(view: View, savedViewState: Bundle?) {
super.onViewCreated(view, savedViewState)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
with(view) {
// Initialize adapter
recycler.layoutManager = LinearLayoutManager(context)
adapter = RecentlyReadAdapter(this@RecentlyReadController)
recycler.setHasFixedSize(true)
recycler.adapter = adapter
}
// Initialize adapter
recycler.layoutManager = LinearLayoutManager(view.context)
adapter = RecentlyReadAdapter(this@RecentlyReadController)
recycler.setHasFixedSize(true)
recycler.adapter = adapter
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
adapter = null
super.onDestroyView(view)
}
/**
@ -80,11 +75,10 @@ class RecentlyReadController : NucleusController<RecentlyReadPresenter>(),
}
override fun onUpdateEmptyView(size: Int) {
val emptyView = view?.empty_view ?: return
if (size > 0) {
emptyView.hide()
empty_view.hide()
} else {
emptyView.show(R.drawable.ic_glasses_black_128dp, R.string.information_no_recent_manga)
empty_view.show(R.drawable.ic_glasses_black_128dp, R.string.information_no_recent_manga)
}
}
@ -108,9 +102,7 @@ class RecentlyReadController : NucleusController<RecentlyReadPresenter>(),
override fun onCoverClick(position: Int) {
val manga = adapter?.getItem(position)?.mch?.manga ?: return
router.pushController(RouterTransaction.with(MangaController(manga))
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler()))
router.pushController(MangaController(manga).withFadeTransaction())
}
override fun removeHistory(manga: Manga, history: History, all: Boolean) {

View File

@ -2,11 +2,11 @@ package eu.kanade.tachiyomi.ui.recently_read
import android.view.View
import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
import eu.kanade.tachiyomi.data.glide.GlideApp
import kotlinx.android.synthetic.main.recently_read_item.view.*
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import kotlinx.android.synthetic.main.recently_read_item.*
import java.util.*
/**
@ -21,18 +21,18 @@ import java.util.*
class RecentlyReadHolder(
view: View,
val adapter: RecentlyReadAdapter
) : FlexibleViewHolder(view, adapter) {
) : BaseFlexibleViewHolder(view, adapter) {
init {
itemView.remove.setOnClickListener {
remove.setOnClickListener {
adapter.removeClickListener.onRemoveClick(adapterPosition)
}
itemView.resume.setOnClickListener {
resume.setOnClickListener {
adapter.resumeClickListener.onResumeClick(adapterPosition)
}
itemView.cover.setOnClickListener {
cover.setOnClickListener {
adapter.coverClickListener.onCoverClick(adapterPosition)
}
}
@ -47,24 +47,24 @@ class RecentlyReadHolder(
val (manga, chapter, history) = item
// Set manga title
itemView.manga_title.text = manga.title
manga_title.text = manga.title
// Set source + chapter title
val formattedNumber = adapter.decimalFormat.format(chapter.chapter_number.toDouble())
itemView.manga_source.text = itemView.context.getString(R.string.recent_manga_source)
manga_source.text = itemView.context.getString(R.string.recent_manga_source)
.format(adapter.sourceManager.get(manga.source)?.toString(), formattedNumber)
// Set last read timestamp title
itemView.last_read.text = adapter.dateFormat.format(Date(history.last_read))
last_read.text = adapter.dateFormat.format(Date(history.last_read))
// Set cover
GlideApp.with(itemView.context).clear(itemView.cover)
GlideApp.with(itemView.context).clear(cover)
if (!manga.thumbnail_url.isNullOrEmpty()) {
GlideApp.with(itemView.context)
.load(manga)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop()
.into(itemView.cover)
.into(cover)
}
}

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.setting
import android.content.Context
import android.support.graphics.drawable.VectorDrawableCompat
import android.support.v4.graphics.drawable.DrawableCompat
import android.support.v7.preference.*
@ -10,7 +9,7 @@ import eu.kanade.tachiyomi.widget.preference.IntListPreference
@Target(AnnotationTarget.TYPE)
annotation class DSL
inline fun PreferenceManager.newScreen(context: Context, block: (@DSL PreferenceScreen).() -> Unit): PreferenceScreen {
inline fun PreferenceManager.newScreen(block: (@DSL PreferenceScreen).() -> Unit): PreferenceScreen {
return createPreferenceScreen(context).also { it.block() }
}

View File

@ -1,6 +1,8 @@
package eu.kanade.tachiyomi.ui.setting
import android.app.Dialog
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.support.v7.preference.PreferenceScreen
import android.view.View
@ -9,8 +11,8 @@ import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.updater.GithubUpdateChecker
import eu.kanade.tachiyomi.data.updater.GithubUpdateResult
import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob
import eu.kanade.tachiyomi.data.updater.UpdateDownloaderService
import eu.kanade.tachiyomi.data.updater.UpdaterJob
import eu.kanade.tachiyomi.data.updater.UpdaterService
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.util.toast
import rx.Subscription
@ -22,8 +24,6 @@ import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
import android.content.Intent
import android.net.Uri
class SettingsAboutController : SettingsController() {
@ -59,9 +59,9 @@ class SettingsAboutController : SettingsController() {
onChange { newValue ->
val checked = newValue as Boolean
if (checked) {
UpdateCheckerJob.setupTask()
UpdaterJob.setupTask()
} else {
UpdateCheckerJob.cancelTask()
UpdaterJob.cancelTask()
}
true
}
@ -71,7 +71,7 @@ class SettingsAboutController : SettingsController() {
}
preference {
title = "Discord"
val url = "https://discord.gg/WrBkRk4"
val url = "https://discord.gg/2dDQBv2"
summary = url
onClick {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
@ -148,7 +148,7 @@ class SettingsAboutController : SettingsController() {
if (appContext != null) {
// Start download
val url = args.getString(URL_KEY)
UpdateDownloaderService.downloadUpdate(appContext, url)
UpdaterService.downloadUpdate(appContext, url)
}
}
.build()

View File

@ -10,8 +10,11 @@ import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.base.controller.BaseController
import rx.Observable
import rx.Subscription
import rx.subscriptions.CompositeSubscription
@ -55,9 +58,23 @@ abstract class SettingsController : PreferenceController() {
return preferenceScreen?.title?.toString()
}
override fun onAttach(view: View) {
fun setTitle() {
var parentController = parentController
while (parentController != null) {
if (parentController is BaseController && parentController.getTitle() != null) {
return
}
parentController = parentController.parentController
}
(activity as? AppCompatActivity)?.supportActionBar?.title = getTitle()
super.onAttach(view)
}
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
if (type.isEnter) {
setTitle()
}
super.onChangeStarted(handler, type)
}
fun <T> Observable<T>.subscribeUntilDestroy(): Subscription {

View File

@ -1,9 +1,8 @@
package eu.kanade.tachiyomi.ui.setting
import android.support.v7.preference.PreferenceScreen
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.util.getResourceColor
class SettingsMainController : SettingsController() {
@ -57,8 +56,6 @@ class SettingsMainController : SettingsController() {
}
private fun navigateTo(controller: SettingsController) {
router.pushController(RouterTransaction.with(controller)
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler()))
router.pushController(controller.withFadeTransaction())
}
}

View File

@ -3,8 +3,6 @@ package eu.kanade.tachiyomi.ui.setting
import android.graphics.drawable.Drawable
import android.support.v7.preference.PreferenceGroup
import android.support.v7.preference.PreferenceScreen
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.SourceManager

View File

@ -13,9 +13,8 @@ import java.io.File
* @param context context of application
*/
fun File.getUriCompat(context: Context): Uri {
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", this)
else Uri.fromFile(this)
return uri
}

View File

@ -0,0 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#FFFFFF" android:pathData="M17,18V5H7V18L12,15.82L17,18M17,3A2,2 0 0,1 19,5V21L12,18L5,21V5C5,3.89 5.9,3 7,3H17M11,7H13V9H15V11H13V13H11V11H9V9H11V7Z" />
</vector>

View File

@ -0,0 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#FFFFFF" android:pathData="M12,8A3,3 0 0,0 15,5A3,3 0 0,0 12,2A3,3 0 0,0 9,5A3,3 0 0,0 12,8M12,11.54C9.64,9.35 6.5,8 3,8V19C6.5,19 9.64,20.35 12,22.54C14.36,20.35 17.5,19 21,19V8C17.5,8 14.36,9.35 12,11.54Z" />
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="128dp"
android:height="128dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M11,13.5V21.5H3V13.5H11M12,2L17.5,11H6.5L12,2M17.5,13C20,13 22,15 22,17.5C22,20 20,22 17.5,22C15,22 13,20 13,17.5C13,15 15,13 17.5,13Z" />
</vector>

View File

@ -3,7 +3,7 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="eu.kanade.tachiyomi.ui.catalogue.CatalogueController"
tools:context="eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController"
android:id="@id/swipe_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent">

View File

@ -11,7 +11,7 @@
android:fitsSystemWindows="true"
android:orientation="vertical"
android:id="@+id/catalogue_view"
tools:context="eu.kanade.tachiyomi.ui.catalogue.CatalogueController">
tools:context="eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController">
<ProgressBar
android:id="@+id/progress"

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<eu.kanade.tachiyomi.ui.catalogue.CatalogueNavigationView
<eu.kanade.tachiyomi.ui.catalogue.browse.CatalogueNavigationView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/nav_view2"
android:layout_width="wrap_content"

View File

@ -10,5 +10,6 @@ android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:paddingBottom="4dp"
android:paddingTop="4dp"
android:clipToPadding="false"
tools:listitem="@layout/catalogue_global_search_controller_card" />
</FrameLayout>

View File

@ -78,6 +78,7 @@
android:orientation="horizontal"
android:paddingEnd="4dp"
android:paddingStart="4dp"
android:clipToPadding="false"
tools:listitem="@layout/catalogue_global_search_controller_card_item" />
</android.support.v7.widget.CardView>
</android.support.constraint.ConstraintLayout>

View File

@ -7,4 +7,5 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:columnWidth="140dp"
android:clipToPadding="false"
tools:listitem="@layout/catalogue_grid_item" />

Some files were not shown because too many files have changed in this diff Show More