Compare commits

..

1 Commits

Author SHA1 Message Date
Jobobby04 cbf82a9d6a Hide dedupe by priority 2021-04-11 21:53:50 -04:00
189 changed files with 1506 additions and 1884 deletions
+1
View File
@@ -1 +1,2 @@
github: inorichi
ko_fi: inorichi ko_fi: inorichi
+2 -8
View File
@@ -2,15 +2,9 @@
I acknowledge that: I acknowledge that:
- I have updated: - I have updated to the latest version of the app (stable is v1.6.0)
- To the latest version of the app (stable is v1.6.2) - I have updated all extensions
- All extensions
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions - If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open issue
- I will fill out the title and the information in this template
Note that the issue will be automatically closed if you do not fill out the title or requested information.
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT** **DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
+2 -8
View File
@@ -9,15 +9,9 @@ labels: "bug"
I acknowledge that: I acknowledge that:
- I have updated: - I have updated to the latest version of the app (stable is v1.6.0)
- To the latest version of the app (stable is v1.6.2) - I have updated all extensions
- All extensions
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions - If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open issue
- I will fill out the title and the information in this template
Note that the issue will be automatically closed if you do not fill out the title or requested information.
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT** **DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
+2 -7
View File
@@ -9,14 +9,9 @@ labels: "feature"
I acknowledge that: I acknowledge that:
- I have updated: - I have updated to the latest version of the app (stable is v1.6.0)
- To the latest version of the app (stable is v1.6.2) - I have updated all extensions
- All extensions
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions - If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open issue
- I will fill out the title and the information in this template
Note that the issue will be automatically closed if you do not fill out the title or requested information.
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT** **DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
Binary file not shown.

Before

Width:  |  Height:  |  Size: 489 KiB

After

Width:  |  Height:  |  Size: 1.7 MiB

+1 -1
View File
@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Autoclose issues - name: Autoclose issues
uses: arkon/issue-closer-action@v3.1 uses: arkon/issue-closer-action@v3.0
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
rules: | rules: |
+1 -1
View File
@@ -11,7 +11,7 @@ Tachiyomi is a free and open source manga reader for Android 5.0 and above. This
## Features ## Features
Features of Tachiyomi(original) include: Features of Tachiyomi(original) include:
* Online reading from [a variety of sources](https://github.com/tachiyomiorg/tachiyomi-extensions) * Online reading from sources such as MangaDex, MangaSee, Mangakakalot, [and more](https://github.com/tachiyomiorg/tachiyomi-extensions)
* Local reading of downloaded manga * Local reading of downloaded manga
* A configurable reader with multiple viewers, reading directions and other settings. * A configurable reader with multiple viewers, reading directions and other settings.
* [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/) support * [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/) support
+7 -7
View File
@@ -34,8 +34,8 @@ android {
minSdkVersion(AndroidConfig.minSdk) minSdkVersion(AndroidConfig.minSdk)
targetSdkVersion(AndroidConfig.targetSdk) targetSdkVersion(AndroidConfig.targetSdk)
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
versionCode = 16 versionCode = 14
versionName = "1.6.2" versionName = "1.6.0"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"") buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
@@ -160,7 +160,7 @@ dependencies {
implementation("com.github.pwittchen:reactivenetwork:0.13.0") implementation("com.github.pwittchen:reactivenetwork:0.13.0")
// Network client // Network client
val okhttpVersion = "4.9.1" val okhttpVersion = "5.0.0-alpha.2"
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion") implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion") implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion") implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
@@ -170,7 +170,7 @@ dependencies {
implementation("org.conscrypt:conscrypt-android:2.5.1") implementation("org.conscrypt:conscrypt-android:2.5.1")
// JSON // JSON
val kotlinSerializationVersion = "1.1.0" val kotlinSerializationVersion = "1.0.1"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
implementation("com.google.code.gson:gson:2.8.6") implementation("com.google.code.gson:gson:2.8.6")
@@ -181,7 +181,7 @@ dependencies {
// Disk // Disk
implementation("com.jakewharton:disklrucache:2.0.2") implementation("com.jakewharton:disklrucache:2.0.2")
implementation("com.github.tachiyomiorg:unifile:17bec43") implementation("com.github.inorichi:unifile:e9ee588")
implementation("com.github.junrar:junrar:7.4.0") implementation("com.github.junrar:junrar:7.4.0")
// HTML parser // HTML parser
@@ -267,12 +267,12 @@ dependencies {
implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN)) implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN))
val coroutinesVersion = "1.4.3" val coroutinesVersion = "1.4.2"
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
// For detecting memory leaks; see https://square.github.io/leakcanary/ // For detecting memory leaks; see https://square.github.io/leakcanary/
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.7") // debugImplementation("com.squareup.leakcanary:leakcanary-android:2.6")
// SY --> // SY -->
// [EXH] Android 7 SSL Workaround // [EXH] Android 7 SSL Workaround
+4 -4
View File
@@ -33,7 +33,7 @@
android:largeHeap="true" android:largeHeap="true"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/Theme.Base" android:theme="@style/Theme.Tachiyomi.Light"
android:networkSecurityConfig="@xml/network_security_config"> android:networkSecurityConfig="@xml/network_security_config">
<activity <activity
android:name=".ui.main.MainActivity" android:name=".ui.main.MainActivity"
@@ -85,7 +85,7 @@
</activity> </activity>
<activity <activity
android:name=".ui.security.BiometricUnlockActivity" android:name=".ui.security.BiometricUnlockActivity"
android:theme="@style/Theme.Base" /> android:theme="@style/Theme.Splash" />
<activity <activity
android:name=".ui.webview.WebViewActivity" android:name=".ui.webview.WebViewActivity"
android:configChanges="uiMode|orientation|screenSize" /> android:configChanges="uiMode|orientation|screenSize" />
@@ -200,7 +200,7 @@
<activity <activity
android:name="exh.ui.intercept.InterceptActivity" android:name="exh.ui.intercept.InterceptActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/Theme.Base"> android:theme="@style/Theme.EHActivity">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@@ -374,7 +374,7 @@
</activity> </activity>
<activity <activity
android:name="exh.ui.captcha.BrowserActionActivity" android:name="exh.ui.captcha.BrowserActionActivity"
android:theme="@style/Theme.Base" /> android:theme="@style/Theme.EHActivity" />
</application> </application>
</manifest> </manifest>
@@ -82,9 +82,6 @@ open class App : Application(), LifecycleObserver {
LocaleHelper.updateConfiguration(this, resources.configuration) LocaleHelper.updateConfiguration(this, resources.configuration)
ProcessLifecycleOwner.get().lifecycle.addObserver(this) ProcessLifecycleOwner.get().lifecycle.addObserver(this)
// Reset Incognito Mode on relaunch
preferences.incognitoMode().set(false)
} }
override fun attachBaseContext(base: Context) { override fun attachBaseContext(base: Context) {
@@ -1,6 +1,5 @@
package eu.kanade.tachiyomi package eu.kanade.tachiyomi
import android.os.Build
import androidx.core.content.edit import androidx.core.content.edit
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
@@ -142,15 +141,6 @@ object Migrations {
} }
} }
} }
if (oldVersion < 59) {
// Reset rotation to Free after replacing Lock
preferences.rotation().set(1)
// Disable update check for Android 5.x users
if (BuildConfig.INCLUDE_UPDATER && Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
UpdaterJob.cancelTask(context)
}
}
return true return true
} }
@@ -2,6 +2,8 @@ package eu.kanade.tachiyomi.data.cache
import android.content.Context import android.content.Context
import android.text.format.Formatter import android.text.format.Formatter
import com.github.salomonbrys.kotson.fromJson
import com.google.gson.Gson
import com.jakewharton.disklrucache.DiskLruCache import com.jakewharton.disklrucache.DiskLruCache
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@@ -13,12 +15,10 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.Response import okhttp3.Response
import okio.buffer import okio.buffer
import okio.sink import okio.sink
import rx.Observable
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
@@ -48,12 +48,14 @@ class ChapterCache(private val context: Context) {
private val scope = CoroutineScope(Job() + Dispatchers.Main) private val scope = CoroutineScope(Job() + Dispatchers.Main)
/** Google Json class used for parsing JSON files. */ /** Google Json class used for parsing JSON files. */
private val json: Json by injectLazy() private val gson: Gson by injectLazy()
// --> EH // --> EH
private val prefs: PreferencesHelper by injectLazy() private val prefs: PreferencesHelper by injectLazy()
// <-- EH
/** Cache class used for cache management. */ /** Cache class used for cache management. */
// --> EH
private var diskCache = setupDiskCache(prefs.cacheSize().get().toLong()) private var diskCache = setupDiskCache(prefs.cacheSize().get().toLong())
init { init {
@@ -71,7 +73,7 @@ class ChapterCache(private val context: Context) {
/** /**
* Returns directory of cache. * Returns directory of cache.
*/ */
private val cacheDir: File val cacheDir: File
get() = diskCache.directory get() = diskCache.directory
/** /**
@@ -98,19 +100,43 @@ class ChapterCache(private val context: Context) {
} }
// <-- EH // <-- EH
/**
* Remove file from cache.
*
* @param file name of file "md5.0".
* @return status of deletion for the file.
*/
fun removeFileFromCache(file: String): Boolean {
// Make sure we don't delete the journal file (keeps track of cache).
if (file == "journal" || file.startsWith("journal.")) {
return false
}
return try {
// Remove the extension from the file to get the key of the cache
val key = file.substringBeforeLast(".")
// Remove file from cache.
diskCache.remove(key)
} catch (e: Exception) {
false
}
}
/** /**
* Get page list from cache. * Get page list from cache.
* *
* @param chapter the chapter. * @param chapter the chapter.
* @return the list of pages. * @return an observable of the list of pages.
*/ */
fun getPageListFromCache(chapter: Chapter): List<Page> { fun getPageListFromCache(chapter: Chapter): Observable<List<Page>> {
// Get the key for the chapter. return Observable.fromCallable {
val key = DiskUtil.hashKeyForDisk(getKey(chapter)) // Get the key for the chapter.
val key = DiskUtil.hashKeyForDisk(getKey(chapter))
// Convert JSON string to list of objects. Throws an exception if snapshot is null // Convert JSON string to list of objects. Throws an exception if snapshot is null
return diskCache.get(key).use { diskCache.get(key).use {
json.decodeFromString(it.getString(0)) gson.fromJson<List<Page>>(it.getString(0))
}
} }
} }
@@ -122,7 +148,7 @@ class ChapterCache(private val context: Context) {
*/ */
fun putPageListToCache(chapter: Chapter, pages: List<Page>) { fun putPageListToCache(chapter: Chapter, pages: List<Page>) {
// Convert list of pages to json string. // Convert list of pages to json string.
val cachedValue = json.encodeToString(pages) val cachedValue = gson.toJson(pages)
// Initialize the editor (edits the values for an entry). // Initialize the editor (edits the values for an entry).
var editor: DiskLruCache.Editor? = null var editor: DiskLruCache.Editor? = null
@@ -202,38 +228,6 @@ class ChapterCache(private val context: Context) {
} }
} }
fun clear(): Int {
var deletedFiles = 0
cacheDir.listFiles()?.forEach {
if (removeFileFromCache(it.name)) {
deletedFiles++
}
}
return deletedFiles
}
/**
* Remove file from cache.
*
* @param file name of file "md5.0".
* @return status of deletion for the file.
*/
private fun removeFileFromCache(file: String): Boolean {
// Make sure we don't delete the journal file (keeps track of cache).
if (file == "journal" || file.startsWith("journal.")) {
return false
}
return try {
// Remove the extension from the file to get the key of the cache
val key = file.substringBeforeLast(".")
// Remove file from cache.
diskCache.remove(key)
} catch (e: Exception) {
false
}
}
private fun getKey(chapter: Chapter): String { private fun getKey(chapter: Chapter): String {
return "${chapter.manga_id}${chapter.url}" return "${chapter.manga_id}${chapter.url}"
} }
@@ -263,7 +263,7 @@ class DownloadManager(private val context: Context) {
if (removeNonFavorite && !manga.favorite) { if (removeNonFavorite && !manga.favorite) {
val mangaFolder = provider.getMangaDir(manga, source) val mangaFolder = provider.getMangaDir(manga, source)
cleaned += 1 + mangaFolder.listFiles().orEmpty().size cleaned += 1 + (mangaFolder.listFiles()?.size ?: 0)
mangaFolder.delete() mangaFolder.delete()
cache.removeManga(manga) cache.removeManga(manga)
return cleaned return cleaned
@@ -284,7 +284,8 @@ class DownloadManager(private val context: Context) {
if (cache.getDownloadCount(manga) == 0) { if (cache.getDownloadCount(manga) == 0) {
val mangaFolder = provider.getMangaDir(manga, source) val mangaFolder = provider.getMangaDir(manga, source)
if (!mangaFolder.listFiles().isNullOrEmpty()) { val size = mangaFolder.listFiles()?.size ?: 0
if (size == 0) {
mangaFolder.delete() mangaFolder.delete()
cache.removeManga(manga) cache.removeManga(manga)
} else { } else {
@@ -28,6 +28,7 @@ internal class DownloadNotifier(private val context: Context) {
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) { context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)) setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
setAutoCancel(false) setAutoCancel(false)
setOngoing(true)
setOnlyAlertOnce(true) setOnlyAlertOnce(true)
} }
} }
@@ -83,6 +84,7 @@ internal class DownloadNotifier(private val context: Context) {
*/ */
fun onProgressChange(download: Download) { fun onProgressChange(download: Download) {
with(progressNotificationBuilder) { with(progressNotificationBuilder) {
// Check if first call.
if (!isDownloading) { if (!isDownloading) {
setSmallIcon(android.R.drawable.stat_sys_download) setSmallIcon(android.R.drawable.stat_sys_download)
clearActions() clearActions()
@@ -114,7 +116,6 @@ internal class DownloadNotifier(private val context: Context) {
} }
setProgress(download.pages!!.size, download.downloadedImages, false) setProgress(download.pages!!.size, download.downloadedImages, false)
setOngoing(true)
show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS) show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
} }
@@ -129,7 +130,6 @@ internal class DownloadNotifier(private val context: Context) {
setContentText(context.getString(R.string.download_notifier_download_paused)) setContentText(context.getString(R.string.download_notifier_download_paused))
setSmallIcon(R.drawable.ic_pause_24dp) setSmallIcon(R.drawable.ic_pause_24dp)
setProgress(0, 0, false) setProgress(0, 0, false)
setOngoing(false)
clearActions() clearActions()
// Open download manager when clicked // Open download manager when clicked
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
@@ -65,7 +65,7 @@ class DownloadProvider(private val context: Context) {
* @param source the source to query. * @param source the source to query.
*/ */
fun findSourceDir(source: Source): UniFile? { fun findSourceDir(source: Source): UniFile? {
return downloadsDir.findFile(getSourceDirName(source), true) return downloadsDir.findFile(getSourceDirName(source))
} }
/** /**
@@ -76,7 +76,7 @@ class DownloadProvider(private val context: Context) {
*/ */
fun findMangaDir(manga: Manga, source: Source): UniFile? { fun findMangaDir(manga: Manga, source: Source): UniFile? {
val sourceDir = findSourceDir(source) val sourceDir = findSourceDir(source)
return sourceDir?.findFile(getMangaDirName(manga), true) return sourceDir?.findFile(getMangaDirName(manga))
} }
/** /**
@@ -89,7 +89,7 @@ class DownloadProvider(private val context: Context) {
fun findChapterDir(chapter: Chapter, manga: Manga, source: Source): UniFile? { fun findChapterDir(chapter: Chapter, manga: Manga, source: Source): UniFile? {
val mangaDir = findMangaDir(manga, source) val mangaDir = findMangaDir(manga, source)
return getValidChapterDirNames(chapter).asSequence() return getValidChapterDirNames(chapter).asSequence()
.mapNotNull { mangaDir?.findFile(it, true) ?: mangaDir?.findFile("$it.cbz", true) } .mapNotNull { mangaDir?.findFile(it) ?: mangaDir?.findFile("$it.cbz") }
.firstOrNull() .firstOrNull()
} }
@@ -123,12 +123,14 @@ class DownloadProvider(private val context: Context) {
source: Source source: Source
): List<UniFile> { ): List<UniFile> {
val mangaDir = findMangaDir(manga, source) ?: return emptyList() val mangaDir = findMangaDir(manga, source) ?: return emptyList()
return mangaDir.listFiles().orEmpty().asList().filter { return mangaDir.listFiles()!!.asList().filter {
chapters.find { chp -> (
getValidChapterDirNames(chp).any { dir -> chapters.find { chp ->
mangaDir.findFile(dir) ?: mangaDir.findFile("$dir.cbz") != null getValidChapterDirNames(chp).any { dir ->
} mangaDir.findFile(dir) ?: mangaDir.findFile("$dir.cbz") != null
} == null || it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true }
} == null
) || it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true
} }
} }
// SY <-- // SY <--
@@ -139,7 +141,7 @@ class DownloadProvider(private val context: Context) {
* @param source the source to query. * @param source the source to query.
*/ */
fun getSourceDirName(source: Source): String { fun getSourceDirName(source: Source): String {
return DiskUtil.buildValidFilename(source.toString()) return source.toString()
} }
/** /**
@@ -176,7 +178,6 @@ class DownloadProvider(private val context: Context) {
return listOf( return listOf(
getChapterDirName(chapter), getChapterDirName(chapter),
// TODO: remove this
// Legacy chapter directory name used in v0.9.2 and before // Legacy chapter directory name used in v0.9.2 and before
DiskUtil.buildValidFilename(chapter.name) DiskUtil.buildValidFilename(chapter.name)
) )
@@ -24,7 +24,6 @@ object PreferenceValues {
enum class DarkThemeVariant { enum class DarkThemeVariant {
default, default,
blue, blue,
amoledblue,
amoled, amoled,
red, red,
midnightdusk, midnightdusk,
@@ -11,6 +11,9 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t
private val json: Json by injectLazy() private val json: Json by injectLazy()
private var oauth: OAuth? = null private var oauth: OAuth? = null
set(value) {
field = value?.copy(expires_in = System.currentTimeMillis() + (value.expires_in * 1000))
}
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request() val originalRequest = chain.request()
@@ -21,19 +24,21 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t
if (oauth == null) { if (oauth == null) {
oauth = myanimelist.loadOAuth() oauth = myanimelist.loadOAuth()
} }
// Refresh access token if expired // Refresh access token if null or expired.
if (oauth != null && oauth!!.isExpired()) { if (oauth!!.isExpired()) {
chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!.refresh_token)).use { chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!.refresh_token)).use {
if (it.isSuccessful) { if (it.isSuccessful) {
setAuth(json.decodeFromString(it.body!!.string())) setAuth(json.decodeFromString(it.body!!.string()))
} }
} }
} }
// Throw on null auth.
if (oauth == null) { if (oauth == null) {
throw Exception("No authentication token") throw Exception("No authentication token")
} }
// Add the authorization header to the original request // Add the authorization header to the original request.
val authRequest = originalRequest.newBuilder() val authRequest = originalRequest.newBuilder()
.addHeader("Authorization", "Bearer ${oauth!!.access_token}") .addHeader("Authorization", "Bearer ${oauth!!.access_token}")
.build() .build()
@@ -7,9 +7,8 @@ data class OAuth(
val refresh_token: String, val refresh_token: String,
val access_token: String, val access_token: String,
val token_type: String, val token_type: String,
val created_at: Long = System.currentTimeMillis(),
val expires_in: Long val expires_in: Long
) { ) {
fun isExpired() = System.currentTimeMillis() > created_at + (expires_in * 1000) fun isExpired() = System.currentTimeMillis() > expires_in
} }
@@ -1,6 +1,9 @@
package eu.kanade.tachiyomi.data.updater package eu.kanade.tachiyomi.data.updater
import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent
import androidx.core.app.NotificationCompat
import androidx.work.Constraints import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType import androidx.work.NetworkType
@@ -8,26 +11,52 @@ import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.Worker import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.updater.github.GithubUpdateChecker import eu.kanade.tachiyomi.data.updater.github.GithubUpdateChecker
import eu.kanade.tachiyomi.util.system.notificationManager
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class UpdaterJob(private val context: Context, workerParams: WorkerParameters) : class UpdaterJob(private val context: Context, workerParams: WorkerParameters) :
Worker(context, workerParams) { Worker(context, workerParams) {
override fun doWork() = runBlocking { override fun doWork(): Result {
try { return runBlocking {
val result = GithubUpdateChecker().checkForUpdate() try {
val result = GithubUpdateChecker().checkForUpdate()
if (result is UpdateResult.NewUpdate<*>) { if (result is UpdateResult.NewUpdate<*>) {
UpdaterNotifier(context).promptUpdate(result.release.downloadLink) 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)
)
}
}
Result.success()
} catch (e: Exception) {
Result.failure()
} }
Result.success()
} catch (e: Exception) {
Result.failure()
} }
} }
fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) {
block()
context.notificationManager.notify(Notifications.ID_UPDATER, build())
}
companion object { companion object {
private const val TAG = "UpdateChecker" private const val TAG = "UpdateChecker"
@@ -1,8 +1,6 @@
package eu.kanade.tachiyomi.data.updater package eu.kanade.tachiyomi.data.updater
import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@@ -30,27 +28,6 @@ internal class UpdaterNotifier(private val context: Context) {
context.notificationManager.notify(id, build()) context.notificationManager.notify(id, build())
} }
fun promptUpdate(url: String) {
val intent = Intent(context, UpdaterService::class.java).apply {
putExtra(UpdaterService.EXTRA_DOWNLOAD_URL, url)
}
val pendingIntent = PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
with(notificationBuilder) {
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)
setContentIntent(pendingIntent)
clearActions()
addAction(
android.R.drawable.stat_sys_download_done,
context.getString(R.string.action_download),
pendingIntent
)
}
notificationBuilder.show()
}
/** /**
* Call when apk download starts. * Call when apk download starts.
* *
@@ -86,20 +63,19 @@ internal class UpdaterNotifier(private val context: Context) {
* @param uri path location of apk. * @param uri path location of apk.
*/ */
fun onDownloadFinished(uri: Uri) { fun onDownloadFinished(uri: Uri) {
val installIntent = NotificationHandler.installApkPendingActivity(context, uri)
with(notificationBuilder) { with(notificationBuilder) {
setContentText(context.getString(R.string.update_check_notification_download_complete)) setContentText(context.getString(R.string.update_check_notification_download_complete))
setSmallIcon(android.R.drawable.stat_sys_download_done) setSmallIcon(android.R.drawable.stat_sys_download_done)
setOnlyAlertOnce(false) setOnlyAlertOnce(false)
setProgress(0, 0, false) setProgress(0, 0, false)
setContentIntent(installIntent) // Install action
setContentIntent(NotificationHandler.installApkPendingActivity(context, uri))
clearActions()
addAction( addAction(
R.drawable.ic_system_update_alt_white_24dp, R.drawable.ic_system_update_alt_white_24dp,
context.getString(R.string.action_install), context.getString(R.string.action_install),
installIntent NotificationHandler.installApkPendingActivity(context, uri)
) )
// Cancel action
addAction( addAction(
R.drawable.ic_close_24dp, R.drawable.ic_close_24dp,
context.getString(R.string.action_cancel), context.getString(R.string.action_cancel),
@@ -120,13 +96,13 @@ internal class UpdaterNotifier(private val context: Context) {
setSmallIcon(android.R.drawable.stat_sys_warning) setSmallIcon(android.R.drawable.stat_sys_warning)
setOnlyAlertOnce(false) setOnlyAlertOnce(false)
setProgress(0, 0, false) setProgress(0, 0, false)
// Retry action
clearActions()
addAction( addAction(
R.drawable.ic_refresh_24dp, R.drawable.ic_refresh_24dp,
context.getString(R.string.action_retry), context.getString(R.string.action_retry),
UpdaterService.downloadApkPendingService(context, url) UpdaterService.downloadApkPendingService(context, url)
) )
// Cancel action
addAction( addAction(
R.drawable.ic_close_24dp, R.drawable.ic_close_24dp,
context.getString(R.string.action_cancel), context.getString(R.string.action_cancel),
@@ -163,7 +163,7 @@ internal object ExtensionLoader {
else -> throw Exception("Unknown source class type! ${obj.javaClass}") else -> throw Exception("Unknown source class type! ${obj.javaClass}")
} }
} catch (e: Throwable) { } catch (e: Throwable) {
Timber.e(e, "Extension load error: $extName ($it)") Timber.w(e, "Extension load error: $extName ($it)")
return LoadResult.Error(e) return LoadResult.Error(e)
} }
} }
@@ -1,25 +1,17 @@
package eu.kanade.tachiyomi.source.online package eu.kanade.tachiyomi.source.online
import android.app.Activity
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.base.controller.DialogController
interface LoginSource : Source { interface LoginSource : Source {
val requiresLogin: Boolean val needsLogin: Boolean
val twoFactorAuth: AuthSupport
fun isLogged(): Boolean fun isLogged(): Boolean
fun getUsername(): String fun getLoginDialog(source: Source, activity: Activity): DialogController
fun getPassword(): String suspend fun login(username: String, password: String, twoFactorCode: String = ""): Boolean
suspend fun login(username: String, password: String, twoFactorCode: String?): Boolean
suspend fun logout(): Boolean suspend fun logout(): Boolean
enum class AuthSupport {
NOT_SUPPORTED,
SUPPORTED,
REQUIRED
}
} }
@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.source.online.all package eu.kanade.tachiyomi.source.online.all
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.Uri import android.net.Uri
@@ -11,6 +12,7 @@ import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
@@ -24,6 +26,7 @@ import eu.kanade.tachiyomi.source.online.MetadataSource
import eu.kanade.tachiyomi.source.online.RandomMangaSource import eu.kanade.tachiyomi.source.online.RandomMangaSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.base.controller.BaseController import eu.kanade.tachiyomi.ui.base.controller.BaseController
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.lang.runAsObservable import eu.kanade.tachiyomi.util.lang.runAsObservable
import eu.kanade.tachiyomi.util.lang.withIOContext import eu.kanade.tachiyomi.util.lang.withIOContext
@@ -43,6 +46,7 @@ import exh.metadata.metadata.MangaDexSearchMetadata
import exh.source.DelegatedHttpSource import exh.source.DelegatedHttpSource
import exh.ui.metadata.adapters.MangaDexDescriptionAdapter import exh.ui.metadata.adapters.MangaDexDescriptionAdapter
import exh.util.urlImportFetchSearchManga import exh.util.urlImportFetchSearchManga
import exh.widget.preference.MangadexLoginDialog
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.JsonPrimitive
@@ -176,27 +180,21 @@ class MangaDex(delegate: HttpSource, val context: Context) :
return FollowsHandler(client, headers, Injekt.get(), useLowQualityThumbnail()).fetchFollows() return FollowsHandler(client, headers, Injekt.get(), useLowQualityThumbnail()).fetchFollows()
} }
override val requiresLogin: Boolean = true override val needsLogin: Boolean = true
override val twoFactorAuth = LoginSource.AuthSupport.SUPPORTED override fun getLoginDialog(source: Source, activity: Activity): DialogController {
return MangadexLoginDialog(source as MangaDex)
}
override fun isLogged(): Boolean { override fun isLogged(): Boolean {
val httpUrl = MdUtil.baseUrl.toHttpUrl() val httpUrl = MdUtil.baseUrl.toHttpUrl()
return trackManager.mdList.isLogged && network.cookieManager.get(httpUrl).any { it.name == REMEMBER_ME } return trackManager.mdList.isLogged && network.cookieManager.get(httpUrl).any { it.name == REMEMBER_ME }
} }
override fun getUsername(): String {
return trackManager.mdList.getUsername()
}
override fun getPassword(): String {
return trackManager.mdList.getPassword()
}
override suspend fun login( override suspend fun login(
username: String, username: String,
password: String, password: String,
twoFactorCode: String? twoFactorCode: String
): Boolean { ): Boolean {
return withIOContext { return withIOContext {
val formBody = FormBody.Builder().apply { val formBody = FormBody.Builder().apply {
@@ -204,7 +202,7 @@ class MangaDex(delegate: HttpSource, val context: Context) :
add("login_password", password) add("login_password", password)
add("no_js", "1") add("no_js", "1")
add("remember_me", "1") add("remember_me", "1")
add("two_factor", twoFactorCode ?: "") add("two_factor", twoFactorCode)
} }
runCatching { runCatching {
@@ -225,8 +223,6 @@ class MangaDex(delegate: HttpSource, val context: Context) :
} else { } else {
throw Exception("Json data was null") throw Exception("Json data was null")
} }
}.also {
preferences.setTrackCredentials(trackManager.mdList, username, password)
} }
} }
} }
@@ -1,43 +1,68 @@
package eu.kanade.tachiyomi.ui.base.activity package eu.kanade.tachiyomi.ui.base.activity
import android.content.res.Configuration.UI_MODE_NIGHT_MASK import android.content.res.Configuration
import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DarkThemeVariant
import eu.kanade.tachiyomi.data.preference.PreferenceValues.LightThemeVariant
import eu.kanade.tachiyomi.data.preference.PreferenceValues.ThemeMode
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
abstract class BaseThemedActivity : AppCompatActivity() { abstract class BaseThemedActivity : AppCompatActivity() {
val preferences: PreferencesHelper by injectLazy() val preferences: PreferencesHelper by injectLazy()
val isDarkMode: Boolean by lazy {
val themeMode = preferences.themeMode().get()
(themeMode == Values.ThemeMode.dark) ||
(
themeMode == Values.ThemeMode.system &&
(resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES)
)
}
private val lightTheme: Int by lazy {
when (preferences.themeLight().get()) {
Values.LightThemeVariant.blue -> R.style.Theme_Tachiyomi_LightBlue
else -> {
when {
// Light status + navigation bar
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 -> {
R.style.Theme_Tachiyomi_Light_Api27
}
// Light status bar + fallback gray navigation bar
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> {
R.style.Theme_Tachiyomi_Light_Api23
}
// Fallback gray status + navigation bar
else -> {
R.style.Theme_Tachiyomi_Light
}
}
}
}
}
private val darkTheme: Int by lazy {
when (preferences.themeDark().get()) {
Values.DarkThemeVariant.blue -> R.style.Theme_Tachiyomi_DarkBlue
Values.DarkThemeVariant.amoled -> R.style.Theme_Tachiyomi_Amoled
Values.DarkThemeVariant.red -> R.style.Theme_Tachiyomi_Red
Values.DarkThemeVariant.midnightdusk -> R.style.Theme_Tachiyomi_MidnightDusk
Values.DarkThemeVariant.hotpink -> R.style.Theme_Tachiyomi_HotPink
else -> R.style.Theme_Tachiyomi_Dark
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
val isDarkMode = when (preferences.themeMode().get()) { setTheme(
ThemeMode.light -> false when {
ThemeMode.dark -> true isDarkMode -> darkTheme
ThemeMode.system -> resources.configuration.uiMode and UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES else -> lightTheme
}
val themeId = if (isDarkMode) {
when (preferences.themeDark().get()) {
DarkThemeVariant.default -> R.style.Theme_Tachiyomi_Dark
DarkThemeVariant.blue -> R.style.Theme_Tachiyomi_Dark_Blue
DarkThemeVariant.amoledblue -> R.style.Theme_Tachiyomi_Dark_AmoledBlue
DarkThemeVariant.amoled -> R.style.Theme_Tachiyomi_Dark_Amoled
DarkThemeVariant.red -> R.style.Theme_Tachiyomi_Dark_Red
DarkThemeVariant.midnightdusk -> R.style.Theme_Tachiyomi_Dark_MidnightDusk
DarkThemeVariant.hotpink -> R.style.Theme_Tachiyomi_Dark_HotPink
} }
} else { )
when (preferences.themeLight().get()) {
LightThemeVariant.default -> R.style.Theme_Tachiyomi_Light
LightThemeVariant.blue -> R.style.Theme_Tachiyomi_Light_Blue
}
}
setTheme(themeId)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
} }
} }
@@ -19,8 +19,7 @@ import timber.log.Timber
abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) : abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
RestoreViewOnCreateController(bundle) { RestoreViewOnCreateController(bundle) {
protected lateinit var binding: VB lateinit var binding: VB
private set
lateinit var viewScope: CoroutineScope lateinit var viewScope: CoroutineScope
@@ -52,13 +51,12 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
) )
} }
abstract fun createBinding(inflater: LayoutInflater): VB
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View {
binding = createBinding(inflater) return inflateView(inflater, container)
return binding.root
} }
abstract fun inflateView(inflater: LayoutInflater, container: ViewGroup): View
open fun onViewCreated(view: View) {} open fun onViewCreated(view: View) {}
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.browse
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import com.bluelinelabs.conductor.Controller import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeHandler
@@ -50,7 +51,10 @@ class BrowseController :
return resources!!.getString(R.string.browse) return resources!!.getString(R.string.browse)
} }
override fun createBinding(inflater: LayoutInflater) = PagerControllerBinding.inflate(inflater) override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = PagerControllerBinding.inflate(inflater)
return binding.root
}
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
super.onViewCreated(view) super.onViewCreated(view)
@@ -5,6 +5,7 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeHandler
@@ -56,16 +57,18 @@ open class ExtensionController :
return ExtensionPresenter() return ExtensionPresenter()
} }
override fun createBinding(inflater: LayoutInflater) = ExtensionControllerBinding.inflate(inflater) override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = ExtensionControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter { binding.recycler.applyInsetter {
type(navigationBars = true) { type(navigationBars = true) {
padding() padding()
} }
} }
return binding.root
}
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.swipeRefresh.isRefreshing = true binding.swipeRefresh.isRefreshing = true
binding.swipeRefresh.refreshes() binding.swipeRefresh.refreshes()
@@ -4,12 +4,12 @@ import android.annotation.SuppressLint
import android.view.View import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.viewholders.FlexibleViewHolder import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.databinding.SectionHeaderItemBinding import eu.kanade.tachiyomi.databinding.SourceMainControllerCardHeaderBinding
class ExtensionGroupHolder(view: View, adapter: FlexibleAdapter<*>) : class ExtensionGroupHolder(view: View, adapter: FlexibleAdapter<*>) :
FlexibleViewHolder(view, adapter) { FlexibleViewHolder(view, adapter) {
private val binding = SectionHeaderItemBinding.bind(view) private val binding = SourceMainControllerCardHeaderBinding.bind(view)
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun bind(item: ExtensionGroupItem) { fun bind(item: ExtensionGroupItem) {
@@ -19,7 +19,7 @@ data class ExtensionGroupItem(val name: String, val size: Int, val showSize: Boo
* Returns the layout resource of this item. * Returns the layout resource of this item.
*/ */
override fun getLayoutRes(): Int { override fun getLayoutRes(): Int {
return R.layout.section_header_item return R.layout.source_main_controller_card_header
} }
/** /**
@@ -12,6 +12,7 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.appcompat.view.ContextThemeWrapper import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.preference.Preference import androidx.preference.Preference
@@ -64,9 +65,15 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
setHasOptionsMenu(true) setHasOptionsMenu(true)
} }
override fun createBinding(inflater: LayoutInflater): ExtensionDetailControllerBinding { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
val themedInflater = inflater.cloneInContext(getPreferenceThemeContext()) val themedInflater = inflater.cloneInContext(getPreferenceThemeContext())
return ExtensionDetailControllerBinding.inflate(themedInflater) binding = ExtensionDetailControllerBinding.inflate(themedInflater)
binding.extensionPrefsRecycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root
} }
override fun createPresenter(): ExtensionDetailsPresenter { override fun createPresenter(): ExtensionDetailsPresenter {
@@ -81,12 +88,6 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
super.onViewCreated(view) super.onViewCreated(view)
binding.extensionPrefsRecycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
val extension = presenter.extension ?: return val extension = presenter.extension ?: return
val context = view.context val context = view.context
@@ -6,6 +6,7 @@ import android.os.Bundle
import android.util.TypedValue import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.appcompat.view.ContextThemeWrapper import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.preference.DialogPreference import androidx.preference.DialogPreference
@@ -45,9 +46,10 @@ class SourcePreferencesController(bundle: Bundle? = null) :
bundleOf(SOURCE_ID to sourceId) bundleOf(SOURCE_ID to sourceId)
) )
override fun createBinding(inflater: LayoutInflater): SourcePreferencesControllerBinding { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
val themedInflater = inflater.cloneInContext(getPreferenceThemeContext()) val themedInflater = inflater.cloneInContext(getPreferenceThemeContext())
return SourcePreferencesControllerBinding.inflate(themedInflater) binding = SourcePreferencesControllerBinding.inflate(themedInflater)
return binding.root
} }
override fun createPresenter(): SourcePreferencesPresenter { override fun createPresenter(): SourcePreferencesPresenter {
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.browse.latest
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import dev.chrisbanes.insetter.applyInsetter import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@@ -31,6 +32,23 @@ open class LatestController :
*/ */
protected var adapter: LatestAdapter? = null protected var adapter: LatestAdapter? = null
/**
* Initiate the view with [R.layout.global_search_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 {
binding = LatestControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root
}
override fun getTitle(): String? { override fun getTitle(): String? {
return applicationContext?.getString(R.string.latest) return applicationContext?.getString(R.string.latest)
} }
@@ -64,8 +82,6 @@ open class LatestController :
onMangaClick(manga) onMangaClick(manga)
} }
override fun createBinding(inflater: LayoutInflater): LatestControllerBinding = LatestControllerBinding.inflate(inflater)
/** /**
* Called when the view is created * Called when the view is created
* *
@@ -74,12 +90,6 @@ open class LatestController :
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
super.onViewCreated(view) super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
adapter = LatestAdapter(this) adapter = LatestAdapter(this)
// Create recycler and set adapter. // Create recycler and set adapter.
@@ -6,6 +6,7 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@@ -45,16 +46,18 @@ class PreMigrationController(bundle: Bundle? = null) :
override fun getTitle() = view?.context?.getString(R.string.select_sources) override fun getTitle() = view?.context?.getString(R.string.select_sources)
override fun createBinding(inflater: LayoutInflater) = PreMigrationControllerBinding.inflate(inflater) override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = PreMigrationControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter { binding.recycler.applyInsetter {
type(navigationBars = true) { type(navigationBars = true) {
padding() padding()
} }
} }
return binding.root
}
override fun onViewCreated(view: View) {
super.onViewCreated(view)
val ourAdapter = adapter ?: MigrationSourceAdapter( val ourAdapter = adapter ?: MigrationSourceAdapter(
getEnabledSources().map { MigrationSourceItem(it, isEnabled(it.id.toString())) }, getEnabledSources().map { MigrationSourceItem(it, isEnabled(it.id.toString())) },
@@ -8,6 +8,7 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
@@ -83,23 +84,24 @@ class MigrationListController(bundle: Bundle? = null) :
private val throttleManager = EHentaiThrottleManager() private val throttleManager = EHentaiThrottleManager()
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = MigrationListControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root
}
override fun getTitle(): String { override fun getTitle(): String {
return resources?.getString(R.string.migration) + " (${adapter?.items?.count { return resources?.getString(R.string.migration) + " (${adapter?.items?.count {
it.manga.migrationStatus != MigrationStatus.RUNNING it.manga.migrationStatus != MigrationStatus.RUNNING
}}/${adapter?.itemCount ?: 0})" }}/${adapter?.itemCount ?: 0})"
} }
override fun createBinding(inflater: LayoutInflater) = MigrationListControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
super.onViewCreated(view) super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
setTitle() setTitle()
val config = this.config ?: return val config = this.config ?: return
@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
import android.view.MenuItem import android.view.MenuItem
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@@ -103,30 +102,16 @@ class MigrationProcessAdapter(
// Update chapters read // Update chapters read
if (MigrationFlags.hasChapters(flags)) { if (MigrationFlags.hasChapters(flags)) {
val prevMangaChapters = db.getChapters(prevManga).executeAsBlocking() val prevMangaChapters = db.getChapters(prevManga).executeAsBlocking()
val maxChapterRead = val maxChapterRead = prevMangaChapters.filter { it.read }.maxByOrNull { it.chapter_number }?.chapter_number
prevMangaChapters.filter { it.read }.maxOfOrNull { it.chapter_number } if (maxChapterRead != null) {
val dbChapters = db.getChapters(manga).executeAsBlocking() val dbChapters = db.getChapters(manga).executeAsBlocking()
val prevHistoryList = db.getHistoryByMangaId(prevManga.id!!).executeAsBlocking() for (chapter in dbChapters) {
val historyList = mutableListOf<History>() if (chapter.isRecognizedNumber && chapter.chapter_number <= maxChapterRead) {
for (chapter in dbChapters) {
if (chapter.isRecognizedNumber) {
val prevChapter =
prevMangaChapters.find { it.isRecognizedNumber && it.chapter_number == chapter.chapter_number }
if (prevChapter != null) {
chapter.bookmark = prevChapter.bookmark
chapter.read = prevChapter.read
chapter.date_fetch = prevChapter.date_fetch
prevHistoryList.find { it.chapter_id == prevChapter.id }?.let { prevHistory ->
val history = History.create(chapter).apply { last_read = prevHistory.last_read }
historyList.add(history)
}
} else if (maxChapterRead != null && chapter.chapter_number <= maxChapterRead) {
chapter.read = true chapter.read = true
} }
} }
db.insertChapters(dbChapters).executeAsBlocking()
} }
db.insertChapters(dbChapters).executeAsBlocking()
db.updateHistoryLastRead(historyList).executeAsBlocking()
} }
// Update categories // Update categories
if (MigrationFlags.hasCategories(flags)) { if (MigrationFlags.hasCategories(flags)) {
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.browse.migration.manga
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import dev.chrisbanes.insetter.applyInsetter import dev.chrisbanes.insetter.applyInsetter
@@ -51,16 +52,18 @@ class MigrationMangaController :
return MigrationMangaPresenter(sourceId) return MigrationMangaPresenter(sourceId)
} }
override fun createBinding(inflater: LayoutInflater) = MigrationMangaControllerBinding.inflate(inflater) override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = MigrationMangaControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter { binding.recycler.applyInsetter {
type(navigationBars = true) { type(navigationBars = true) {
padding() padding()
} }
} }
return binding.root
}
override fun onViewCreated(view: View) {
super.onViewCreated(view)
adapter = MigrationMangaAdapter(this) adapter = MigrationMangaAdapter(this)
binding.recycler.layoutManager = LinearLayoutManager(view.context) binding.recycler.layoutManager = LinearLayoutManager(view.context)
@@ -5,6 +5,7 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import dev.chrisbanes.insetter.applyInsetter import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
@@ -41,16 +42,18 @@ class MigrationSourcesController :
return MigrationSourcesPresenter() return MigrationSourcesPresenter()
} }
override fun createBinding(inflater: LayoutInflater) = MigrationSourcesControllerBinding.inflate(inflater) override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = MigrationSourcesControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter { binding.recycler.applyInsetter {
type(navigationBars = true) { type(navigationBars = true) {
padding() padding()
} }
} }
return binding.root
}
override fun onViewCreated(view: View) {
super.onViewCreated(view)
adapter = SourceAdapter(this) adapter = SourceAdapter(this)
binding.recycler.layoutManager = LinearLayoutManager(view.context) binding.recycler.layoutManager = LinearLayoutManager(view.context)
@@ -7,7 +7,7 @@ import eu.davidea.flexibleadapter.items.AbstractHeaderItem
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.SectionHeaderItemBinding import eu.kanade.tachiyomi.databinding.SourceMainControllerCardHeaderBinding
/** /**
* Item that contains the selection header. * Item that contains the selection header.
@@ -18,7 +18,7 @@ class SelectionHeader : AbstractHeaderItem<SelectionHeader.Holder>() {
* Returns the layout resource of this item. * Returns the layout resource of this item.
*/ */
override fun getLayoutRes(): Int { override fun getLayoutRes(): Int {
return R.layout.section_header_item return R.layout.source_main_controller_card_header
} }
/** /**
@@ -46,7 +46,7 @@ class SelectionHeader : AbstractHeaderItem<SelectionHeader.Holder>() {
} }
class Holder(view: View, adapter: FlexibleAdapter</* SY --> */ IFlexible<RecyclerView.ViewHolder> /* SY <-- */>) : FlexibleViewHolder(view, adapter) { class Holder(view: View, adapter: FlexibleAdapter</* SY --> */ IFlexible<RecyclerView.ViewHolder> /* SY <-- */>) : FlexibleViewHolder(view, adapter) {
private val binding = SectionHeaderItemBinding.bind(view) private val binding = SourceMainControllerCardHeaderBinding.bind(view)
init { init {
binding.title.text = view.context.getString(/* SY --> */ R.string.select_a_source_to_migrate_from /* SY <-- */) binding.title.text = view.context.getString(/* SY --> */ R.string.select_a_source_to_migrate_from /* SY <-- */)
@@ -3,13 +3,13 @@ package eu.kanade.tachiyomi.ui.browse.source
import android.view.View import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.viewholders.FlexibleViewHolder import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.databinding.SectionHeaderItemBinding import eu.kanade.tachiyomi.databinding.SourceMainControllerCardHeaderBinding
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
class LangHolder(view: View, adapter: FlexibleAdapter<*>) : class LangHolder(view: View, adapter: FlexibleAdapter<*>) :
FlexibleViewHolder(view, adapter) { FlexibleViewHolder(view, adapter) {
private val binding = SectionHeaderItemBinding.bind(view) private val binding = SourceMainControllerCardHeaderBinding.bind(view)
fun bind(item: LangItem) { fun bind(item: LangItem) {
binding.title.text = LocaleHelper.getSourceDisplayName(item.code, itemView.context) binding.title.text = LocaleHelper.getSourceDisplayName(item.code, itemView.context)
@@ -18,7 +18,7 @@ data class LangItem(val code: String) : AbstractHeaderItem<LangHolder>() {
* Returns the layout resource of this item. * Returns the layout resource of this item.
*/ */
override fun getLayoutRes(): Int { override fun getLayoutRes(): Int {
return R.layout.section_header_item return R.layout.source_main_controller_card_header
} }
/** /**
@@ -9,6 +9,7 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
@@ -87,16 +88,25 @@ class SourceController(bundle: Bundle? = null) :
return SourcePresenter(/* SY --> */ controllerMode = mode /* SY <-- */) return SourcePresenter(/* SY --> */ controllerMode = mode /* SY <-- */)
} }
override fun createBinding(inflater: LayoutInflater) = SourceMainControllerBinding.inflate(inflater) /**
* Initiate the view with [R.layout.source_main_controller].
override fun onViewCreated(view: View) { *
super.onViewCreated(view) * @param inflater used to load the layout xml.
* @param container containing parent views.
* @return inflated view.
*/
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = SourceMainControllerBinding.inflate(inflater)
binding.recycler.applyInsetter { binding.recycler.applyInsetter {
type(navigationBars = true) { type(navigationBars = true) {
padding() padding()
} }
} }
return binding.root
}
override fun onViewCreated(view: View) {
super.onViewCreated(view)
adapter = SourceAdapter(this) adapter = SourceAdapter(this)
@@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.databinding.SourceMainControllerCardItemBinding
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.icon import eu.kanade.tachiyomi.source.icon
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.view.setVectorCompat import eu.kanade.tachiyomi.util.view.setVectorCompat
class SourceHolder(private val view: View, val adapter: SourceAdapter /* SY --> */, private val showLatest: Boolean, private val showPins: Boolean /* SY <-- */) : class SourceHolder(private val view: View, val adapter: SourceAdapter /* SY --> */, private val showLatest: Boolean, private val showPins: Boolean /* SY <-- */) :
@@ -51,9 +52,9 @@ class SourceHolder(private val view: View, val adapter: SourceAdapter /* SY -->
binding.pin.isVisible = showPins binding.pin.isVisible = showPins
if (item.isPinned) { if (item.isPinned) {
binding.pin.setVectorCompat(R.drawable.ic_push_pin_24dp, R.attr.colorAccent) binding.pin.setVectorCompat(R.drawable.ic_push_pin_24dp, view.context.getResourceColor(R.attr.colorAccent))
} else { } else {
binding.pin.setVectorCompat(R.drawable.ic_push_pin_outline_24dp, android.R.attr.textColorHint) binding.pin.setVectorCompat(R.drawable.ic_push_pin_outline_24dp, view.context.getResourceColor(android.R.attr.textColorHint))
} }
} }
} }
@@ -61,7 +61,6 @@ import exh.md.similar.ui.EnableMangaDexSimilarDialogController
import exh.savedsearches.EXHSavedSearch import exh.savedsearches.EXHSavedSearch
import exh.source.getMainSource import exh.source.getMainSource
import exh.source.isEhBasedSource import exh.source.isEhBasedSource
import exh.widget.preference.MangadexLoginDialog
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@@ -159,7 +158,10 @@ open class BrowseSourceController(bundle: Bundle) :
// SY <-- // SY <--
} }
override fun createBinding(inflater: LayoutInflater) = SourceControllerBinding.inflate(inflater) override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = SourceControllerBinding.inflate(inflater)
return binding.root
}
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
super.onViewCreated(view) super.onViewCreated(view)
@@ -180,8 +182,8 @@ open class BrowseSourceController(bundle: Bundle) :
preferences.shownMangaDexSimilarAskDialog().set(true) preferences.shownMangaDexSimilarAskDialog().set(true)
} }
if (mainSource is LoginSource && mainSource.requiresLogin && !mainSource.isLogged()) { if (mainSource is LoginSource && mainSource.needsLogin && !mainSource.isLogged()) {
val dialog = MangadexLoginDialog(mainSource) val dialog = mainSource.getLoginDialog(mainSource, activity!!)
dialog.showDialog(router) dialog.showDialog(router)
} }
// SY <-- // SY <--
@@ -409,7 +411,6 @@ open class BrowseSourceController(bundle: Bundle) :
if (router.backstackSize >= 2 && router.backstack[router.backstackSize - 2].controller() is GlobalSearchController) { if (router.backstackSize >= 2 && router.backstack[router.backstackSize - 2].controller() is GlobalSearchController) {
router.popController(this) router.popController(this)
} else { } else {
nonSubmittedQuery = ""
searchWithQuery("") searchWithQuery("")
} }
@@ -9,7 +9,6 @@ import android.view.ViewGroup
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
@@ -38,17 +37,10 @@ class SourceFilterSheet(
// EXH <-- // EXH <--
) : BaseBottomSheetDialog(activity) { ) : BaseBottomSheetDialog(activity) {
private var filterNavView: FilterNavigationView = FilterNavigationView( private var filterNavView: FilterNavigationView
activity,
// SY -->
searches = searches,
source = source,
controller = controller
// SY <--
)
private val sheetBehavior: BottomSheetBehavior<*>
init { init {
filterNavView = FilterNavigationView(activity /* SY --> */, searches = searches, source = source, controller = controller/* SY <-- */)
filterNavView.onFilterClicked = { filterNavView.onFilterClicked = {
onFilterClicked() onFilterClicked()
this.dismiss() this.dismiss()
@@ -64,13 +56,6 @@ class SourceFilterSheet(
// EXH <-- // EXH <--
setContentView(filterNavView) setContentView(filterNavView)
sheetBehavior = BottomSheetBehavior.from(filterNavView.parent as ViewGroup)
}
override fun show() {
super.show()
sheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
} }
fun setFilters(items: List<IFlexible<*>>) { fun setFilters(items: List<IFlexible<*>>) {
@@ -87,15 +72,7 @@ class SourceFilterSheet(
} }
// SY <-- // SY <--
class FilterNavigationView @JvmOverloads constructor( class FilterNavigationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null /* SY --> */, searches: List<EXHSavedSearch> = emptyList(), source: CatalogueSource? = null, controller: BaseController<*>? = null/* SY <-- */) :
context: Context,
attrs: AttributeSet? = null,
// SY -->
searches: List<EXHSavedSearch> = emptyList(),
source: CatalogueSource? = null,
controller: BaseController<*>? = null
// SY <--
) :
SimpleNavigationView(context, attrs) { SimpleNavigationView(context, attrs) {
var onFilterClicked = {} var onFilterClicked = {}
@@ -114,13 +91,8 @@ class SourceFilterSheet(
// SY <-- // SY <--
val adapter: FlexibleAdapter<IFlexible<*>> = FlexibleAdapter<IFlexible<*>>(null) val adapter: FlexibleAdapter<IFlexible<*>> = FlexibleAdapter<IFlexible<*>>(null)
.setDisplayHeadersAtStartUp(true)
private val binding = SourceFilterSheetBinding.inflate( private val binding = SourceFilterSheetBinding.inflate(LayoutInflater.from(context), null, false)
LayoutInflater.from(context),
null,
false
)
init { init {
// SY --> // SY -->
@@ -6,6 +6,7 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@@ -50,7 +51,22 @@ open class GlobalSearchController(
setHasOptionsMenu(true) setHasOptionsMenu(true)
} }
override fun createBinding(inflater: LayoutInflater) = GlobalSearchControllerBinding.inflate(inflater) /**
* Initiate the view with [R.layout.global_search_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 {
binding = GlobalSearchControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root
}
override fun getTitle(): String? { override fun getTitle(): String? {
return presenter.query return presenter.query
@@ -127,12 +143,6 @@ open class GlobalSearchController(
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
super.onViewCreated(view) super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
adapter = GlobalSearchAdapter(this) adapter = GlobalSearchAdapter(this)
// Create recycler and set adapter. // Create recycler and set adapter.
@@ -5,6 +5,7 @@ import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@@ -73,6 +74,18 @@ open class IndexController :
setHasOptionsMenu(true) setHasOptionsMenu(true)
} }
/**
* Initiate the view with [R.layout.latest_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 {
binding = IndexControllerBinding.inflate(inflater)
return binding.root
}
override fun getTitle(): String? { override fun getTitle(): String? {
return source!!.name return source!!.name
} }
@@ -126,8 +139,6 @@ open class IndexController :
} }
} }
override fun createBinding(inflater: LayoutInflater) = IndexControllerBinding.inflate(inflater)
/** /**
* Called when the view is created * Called when the view is created
* *
@@ -4,6 +4,7 @@ import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@@ -67,7 +68,21 @@ class CategoryController :
return resources?.getString(R.string.action_edit_categories) return resources?.getString(R.string.action_edit_categories)
} }
override fun createBinding(inflater: LayoutInflater) = CategoriesControllerBinding.inflate(inflater) /**
* Returns the view of this controller.
*
* @param inflater The layout inflater to create the view from XML.
* @param container The parent view for this one.
*/
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = CategoriesControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root
}
/** /**
* Called after view inflation. Used to initialize the view. * Called after view inflation. Used to initialize the view.
@@ -77,12 +92,6 @@ class CategoryController :
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
super.onViewCreated(view) super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
adapter = CategoryAdapter(this@CategoryController) adapter = CategoryAdapter(this@CategoryController)
binding.recycler.layoutManager = LinearLayoutManager(view.context) binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.setHasFixedSize(true) binding.recycler.setHasFixedSize(true)
@@ -4,6 +4,7 @@ import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@@ -67,7 +68,21 @@ class BiometricTimesController :
return resources?.getString(R.string.biometric_lock_times) return resources?.getString(R.string.biometric_lock_times)
} }
override fun createBinding(inflater: LayoutInflater) = CategoriesControllerBinding.inflate(inflater) /**
* Returns the view of this controller.
*
* @param inflater The layout inflater to create the view from XML.
* @param container The parent view for this one.
*/
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = CategoriesControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root
}
/** /**
* Called after view inflation. Used to initialize the view. * Called after view inflation. Used to initialize the view.
@@ -77,12 +92,6 @@ class BiometricTimesController :
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
super.onViewCreated(view) super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
adapter = BiometricTimesAdapter(this@BiometricTimesController) adapter = BiometricTimesAdapter(this@BiometricTimesController)
binding.recycler.layoutManager = LinearLayoutManager(view.context) binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.setHasFixedSize(true) binding.recycler.setHasFixedSize(true)
@@ -5,6 +5,7 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@@ -73,7 +74,21 @@ class SortTagController :
setHasOptionsMenu(true) setHasOptionsMenu(true)
} }
override fun createBinding(inflater: LayoutInflater) = CategoriesControllerBinding.inflate(inflater) /**
* Returns the view of this controller.
*
* @param inflater The layout inflater to create the view from XML.
* @param container The parent view for this one.
*/
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = CategoriesControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root
}
/** /**
* Called after view inflation. Used to initialize the view. * Called after view inflation. Used to initialize the view.
@@ -83,12 +98,6 @@ class SortTagController :
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
super.onViewCreated(view) super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
adapter = SortTagAdapter(this@SortTagController) adapter = SortTagAdapter(this@SortTagController)
binding.recycler.layoutManager = LinearLayoutManager(view.context) binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.setHasFixedSize(true) binding.recycler.setHasFixedSize(true)
@@ -4,6 +4,7 @@ import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@@ -64,7 +65,21 @@ class RepoController :
return resources?.getString(R.string.action_edit_repos) return resources?.getString(R.string.action_edit_repos)
} }
override fun createBinding(inflater: LayoutInflater) = CategoriesControllerBinding.inflate(inflater) /**
* Returns the view of this controller.
*
* @param inflater The layout inflater to create the view from XML.
* @param container The parent view for this one.
*/
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = CategoriesControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root
}
/** /**
* Called after view inflation. Used to initialize the view. * Called after view inflation. Used to initialize the view.
@@ -74,12 +89,6 @@ class RepoController :
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
super.onViewCreated(view) super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
adapter = RepoAdapter(this@RepoController) adapter = RepoAdapter(this@RepoController)
binding.recycler.layoutManager = LinearLayoutManager(view.context) binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.setHasFixedSize(true) binding.recycler.setHasFixedSize(true)
@@ -4,6 +4,7 @@ import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@@ -65,7 +66,21 @@ class SourceCategoryController :
return resources?.getString(R.string.action_edit_categories) return resources?.getString(R.string.action_edit_categories)
} }
override fun createBinding(inflater: LayoutInflater) = CategoriesControllerBinding.inflate(inflater) /**
* Returns the view of this controller.
*
* @param inflater The layout inflater to create the view from XML.
* @param container The parent view for this one.
*/
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = CategoriesControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root
}
/** /**
* Called after view inflation. Used to initialize the view. * Called after view inflation. Used to initialize the view.
@@ -75,12 +90,6 @@ class SourceCategoryController :
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
super.onViewCreated(view) super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
adapter = SourceCategoryAdapter(this@SourceCategoryController) adapter = SourceCategoryAdapter(this@SourceCategoryController)
binding.recycler.layoutManager = LinearLayoutManager(view.context) binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.setHasFixedSize(true) binding.recycler.setHasFixedSize(true)
@@ -5,6 +5,7 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@@ -54,7 +55,15 @@ class DownloadController :
setHasOptionsMenu(true) setHasOptionsMenu(true)
} }
override fun createBinding(inflater: LayoutInflater) = DownloadControllerBinding.inflate(inflater) override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = DownloadControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root
}
override fun createPresenter(): DownloadPresenter { override fun createPresenter(): DownloadPresenter {
return DownloadPresenter() return DownloadPresenter()
@@ -67,12 +76,6 @@ class DownloadController :
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
super.onViewCreated(view) super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
// Check if download queue is empty and update information accordingly. // Check if download queue is empty and update information accordingly.
setInformationView() setInformationView()
@@ -81,14 +81,15 @@ class DownloadHolder(private val view: View, val adapter: DownloadAdapter) :
private fun showPopupMenu(view: View) { private fun showPopupMenu(view: View) {
view.popupMenu( view.popupMenu(
menuRes = R.menu.download_single, R.menu.download_single,
initMenu = { {
findItem(R.id.move_to_top).isVisible = bindingAdapterPosition != 0 findItem(R.id.move_to_top).isVisible = bindingAdapterPosition != 0
findItem(R.id.move_to_bottom).isVisible = findItem(R.id.move_to_bottom).isVisible =
bindingAdapterPosition != adapter.itemCount - 1 bindingAdapterPosition != adapter.itemCount - 1
}, },
onMenuItemClick = { {
adapter.downloadItemListener.onMenuItemClick(bindingAdapterPosition, this) adapter.downloadItemListener.onMenuItemClick(bindingAdapterPosition, this)
true
} }
) )
} }
@@ -7,6 +7,7 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import android.view.WindowManager import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
@@ -19,7 +20,6 @@ import com.google.android.material.tabs.TabLayout
import com.jakewharton.rxrelay.BehaviorRelay import com.jakewharton.rxrelay.BehaviorRelay
import com.jakewharton.rxrelay.PublishRelay import com.jakewharton.rxrelay.PublishRelay
import com.tfcporciuncula.flow.Preference import com.tfcporciuncula.flow.Preference
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.SelectableAdapter import eu.davidea.flexibleadapter.SelectableAdapter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
@@ -34,6 +34,7 @@ import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController
import eu.kanade.tachiyomi.ui.base.controller.TabbedController import eu.kanade.tachiyomi.ui.base.controller.TabbedController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesController
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
@@ -42,7 +43,6 @@ import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import exh.favorites.FavoritesIntroDialog import exh.favorites.FavoritesIntroDialog
import exh.favorites.FavoritesSyncStatus import exh.favorites.FavoritesSyncStatus
import exh.source.MERGED_SOURCE_ID
import exh.source.PERV_EDEN_EN_SOURCE_ID import exh.source.PERV_EDEN_EN_SOURCE_ID
import exh.source.PERV_EDEN_IT_SOURCE_ID import exh.source.PERV_EDEN_IT_SOURCE_ID
import exh.source.isEhBasedManga import exh.source.isEhBasedManga
@@ -193,17 +193,14 @@ class LibraryController(
return LibraryPresenter() return LibraryPresenter()
} }
override fun createBinding(inflater: LayoutInflater) = LibraryControllerBinding.inflate(inflater) override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = LibraryControllerBinding.inflate(inflater)
return binding.root
}
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
super.onViewCreated(view) super.onViewCreated(view)
binding.actionToolbar.applyInsetter {
type(navigationBars = true) {
margin(bottom = true)
}
}
adapter = LibraryAdapter(this) adapter = LibraryAdapter(this)
binding.libraryPager.adapter = adapter binding.libraryPager.adapter = adapter
binding.libraryPager.pageSelections() binding.libraryPager.pageSelections()
@@ -476,6 +473,9 @@ class LibraryController(
} }
} }
// SY --> // SY -->
R.id.action_source_migration -> {
router.pushController(MigrationSourcesController().withFadeTransaction())
}
R.id.action_sync_favorites -> { R.id.action_sync_favorites -> {
if (preferences.exhShowSyncIntro().get()) { if (preferences.exhShowSyncIntro().get()) {
activity?.let { FavoritesIntroDialog().show(it) } activity?.let { FavoritesIntroDialog().show(it) }
@@ -542,13 +542,9 @@ class LibraryController(
// SY --> // SY -->
R.id.action_migrate -> { R.id.action_migrate -> {
val skipPre = preferences.skipPreMigration().get() val skipPre = preferences.skipPreMigration().get()
val selectedMangaIds = selectedMangas.filterNot { it.source == MERGED_SOURCE_ID }.mapNotNull { it.id } val selectedMangaIds = selectedMangas.mapNotNull { it.id }
destroyActionModeIfNeeded() destroyActionModeIfNeeded()
if (selectedMangaIds.isNotEmpty()) { PreMigrationController.navigateToMigration(skipPre, router, selectedMangaIds)
PreMigrationController.navigateToMigration(skipPre, router, selectedMangaIds)
} else {
activity?.toast(R.string.no_valid_manga)
}
} }
R.id.action_clean -> cleanTitles() R.id.action_clean -> cleanTitles()
R.id.action_push_to_mdlist -> pushToMdList() R.id.action_push_to_mdlist -> pushToMdList()
@@ -14,7 +14,6 @@ import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.marginTop
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceDialogController import androidx.preference.PreferenceDialogController
@@ -49,8 +48,6 @@ import eu.kanade.tachiyomi.ui.recent.history.HistoryController
import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.InternalResourceHelper
import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import exh.EXHMigrations import exh.EXHMigrations
import exh.eh.EHentaiUpdateWorker import exh.eh.EHentaiUpdateWorker
@@ -126,27 +123,23 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
padding(left = true, top = true, right = true) padding(left = true, top = true, right = true)
} }
} }
binding.rootFab.applyInsetter {
type(navigationBars = true) {
margin()
}
}
binding.bottomNav.applyInsetter { binding.bottomNav.applyInsetter {
type(navigationBars = true) { type(navigationBars = true) {
padding() padding()
} }
} }
binding.rootFab.applyInsetter {
type(navigationBars = true) {
margin()
}
}
// Make sure navigation bar is on bottom before we modify it // Make sure navigation bar is on bottom when making it transparent
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, insets -> ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, insets ->
if (insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom > 0) { if (insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom > 0) {
window.navigationBarColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && // Keep scrim on light theme if windowLightNavigationBar is not available
!InternalResourceHelper.getBoolean(this, "config_navBarNeedsScrim", true) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 || isDarkMode) {
) { window.navigationBarColor = Color.TRANSPARENT
Color.TRANSPARENT
} else {
// Set navbar scrim 70% of navigationBarColor
getResourceColor(android.R.attr.navigationBarColor, 0.7F)
} }
} }
insets insets
@@ -155,15 +148,6 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
tabAnimator = ViewHeightAnimator(binding.tabs, 0L) tabAnimator = ViewHeightAnimator(binding.tabs, 0L)
bottomNavAnimator = ViewHeightAnimator(binding.bottomNav) bottomNavAnimator = ViewHeightAnimator(binding.bottomNav)
// If bottom nav is hidden, make it visible again when the app bar is expanded
binding.appbar.addOnOffsetChangedListener(
AppBarLayout.OnOffsetChangedListener { _, verticalOffset ->
if (verticalOffset == 0) {
showBottomNav(true)
}
}
)
// Set behavior of bottom nav // Set behavior of bottom nav
preferences.hideBottomBar() preferences.hideBottomBar()
.asImmediateFlow { setBottomNavBehaviorOnScroll() } .asImmediateFlow { setBottomNavBehaviorOnScroll() }
@@ -187,9 +171,14 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
val controller = router.getControllerWithTag(id.toString()) as? LibraryController val controller = router.getControllerWithTag(id.toString()) as? LibraryController
controller?.showSettingsSheet() controller?.showSettingsSheet()
} }
// SY -->
R.id.nav_updates -> { R.id.nav_updates -> {
router.pushController(DownloadController().withFadeTransaction()) if (router.backstack.lastOrNull()?.controller() !is DownloadController) {
val controller = router.getControllerWithTag(id.toString()) as? UpdatesController
controller?.router?.pushController(DownloadController().withFadeTransaction())
}
} }
// SY <--
} }
} }
true true
@@ -511,7 +500,7 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
fun fixViewToBottom(view: View) { fun fixViewToBottom(view: View) {
val listener = AppBarLayout.OnOffsetChangedListener { appBarLayout, verticalOffset -> val listener = AppBarLayout.OnOffsetChangedListener { appBarLayout, verticalOffset ->
val maxAbsOffset = appBarLayout.measuredHeight - binding.tabs.measuredHeight val maxAbsOffset = appBarLayout.measuredHeight - binding.tabs.measuredHeight
view.translationY = -maxAbsOffset - verticalOffset.toFloat() + appBarLayout.marginTop view.translationY = -maxAbsOffset - verticalOffset.toFloat()
} }
binding.appbar.addOnOffsetChangedListener(listener) binding.appbar.addOnOffsetChangedListener(listener)
fixedViewsToBottom[view] = listener fixedViewsToBottom[view] = listener
@@ -218,8 +218,8 @@ class EditMangaDialog : DialogController {
private fun ChipGroup.setChips(items: List<String>) { private fun ChipGroup.setChips(items: List<String>) {
removeAllViews() removeAllViews()
items.asSequence().map { item -> items.forEach { item ->
Chip(context).apply { val chip = Chip(context).apply {
text = item text = item
isCloseIconVisible = true isCloseIconVisible = true
@@ -228,16 +228,15 @@ class EditMangaDialog : DialogController {
removeView(this) removeView(this)
} }
} }
}.forEach {
addView(it) addView(chip)
} }
val addTagChip = Chip(context).apply { val addTagChip = Chip(context).apply {
setText(R.string.add_tag) setText(R.string.add_tag)
chipIcon = ContextCompat.getDrawable(context, R.drawable.ic_add_24dp)?.apply { chipIcon = ContextCompat.getDrawable(context, R.drawable.ic_add_24dp)
setTint(context.getResourceColor(R.attr.colorAccent)) chipIcon?.setTint(context.getResourceColor(R.attr.colorAccent))
}
textStartPadding = 0F textStartPadding = 0F
clicks().onEach { clicks().onEach {
@@ -17,6 +17,7 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import android.view.animation.DecelerateInterpolator import android.view.animation.DecelerateInterpolator
import android.widget.ImageView import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@@ -275,21 +276,18 @@ class MangaController :
) )
} }
override fun createBinding(inflater: LayoutInflater) = MangaControllerBinding.inflate(inflater) override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = MangaControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter { binding.recycler.applyInsetter {
type(navigationBars = true) { type(navigationBars = true) {
padding() padding()
} }
} }
binding.actionToolbar.applyInsetter { return binding.root
type(navigationBars = true) { }
margin(bottom = true)
} override fun onViewCreated(view: View) {
} super.onViewCreated(view)
if (manga == null || source == null) return if (manga == null || source == null) return
val adapters: MutableList<RecyclerView.Adapter<out RecyclerView.ViewHolder>?> = mutableListOf() val adapters: MutableList<RecyclerView.Adapter<out RecyclerView.ViewHolder>?> = mutableListOf()
@@ -481,7 +479,7 @@ class MangaController :
// Hide options for non-library manga // Hide options for non-library manga
menu.findItem(R.id.action_edit_categories).isVisible = presenter.manga.favorite && presenter.getCategories().isNotEmpty() menu.findItem(R.id.action_edit_categories).isVisible = presenter.manga.favorite && presenter.getCategories().isNotEmpty()
menu.findItem(R.id.action_edit_cover).isVisible = /* SY --> */ false /* presenter.manga.favorite SY <-- */ menu.findItem(R.id.action_edit_cover).isVisible = /* SY --> */ false /* presenter.manga.favorite SY <-- */
menu.findItem(R.id.action_migrate).isVisible = presenter.manga.favorite /* SY --> */ && presenter.manga.source != MERGED_SOURCE_ID /* SY <-- */ menu.findItem(R.id.action_migrate).isVisible = presenter.manga.favorite
// SY --> // SY -->
menu.findItem(R.id.action_edit).isVisible = presenter.manga.favorite || isLocalSource menu.findItem(R.id.action_edit).isVisible = presenter.manga.favorite || isLocalSource
@@ -1147,7 +1145,8 @@ class MangaController :
fun onChapterDownloadUpdate(download: Download) { fun onChapterDownloadUpdate(download: Download) {
chaptersAdapter?.currentItems?.find { it.id == download.chapter.id }?.let { chaptersAdapter?.currentItems?.find { it.id == download.chapter.id }?.let {
chaptersAdapter?.updateItem(it, it.status) chaptersAdapter?.updateItem(it)
chaptersAdapter?.notifyDataSetChanged()
} }
} }
@@ -1271,6 +1270,7 @@ class MangaController :
binding.actionToolbar.findItem(R.id.action_mark_as_unread)?.isVisible = chapters.all { it.chapter.read } binding.actionToolbar.findItem(R.id.action_mark_as_unread)?.isVisible = chapters.all { it.chapter.read }
// Hide FAB to avoid interfering with the bottom action toolbar // Hide FAB to avoid interfering with the bottom action toolbar
// actionFab?.hide()
actionFab?.isVisible = false actionFab?.isVisible = false
} }
return false return false
@@ -1302,6 +1302,10 @@ class MangaController :
chaptersAdapter?.clearSelection() chaptersAdapter?.clearSelection()
selectedChapters.clear() selectedChapters.clear()
actionMode = null actionMode = null
// TODO: there seems to be a bug in MaterialComponents where the [ExtendedFloatingActionButton]
// fails to show up properly
// actionFab?.show()
actionFab?.isVisible = true actionFab?.isVisible = true
} }
@@ -1423,26 +1427,14 @@ class MangaController :
// OVERFLOW MENU DIALOGS // OVERFLOW MENU DIALOGS
private fun getUnreadChaptersSorted(): List<ChapterItem> { private fun getUnreadChaptersSorted() = /* SY --> */ if (presenter.source.isEhBasedSource()) presenter.chapters
val chapters = presenter.chapters .filter { !it.read && it.status == Download.State.NOT_DOWNLOADED }
.sortedWith(presenter.getChapterSort()) .distinctBy { it.name }
.filter { !it.read && it.status == Download.State.NOT_DOWNLOADED } .sortedBy { it.source_order }
.distinctBy { it.name } else /* SY <-- */ presenter.chapters
// SY --> .filter { !it.read && it.status == Download.State.NOT_DOWNLOADED }
.let { .distinctBy { it.name }
if (presenter.source.isEhBasedSource()) { .sortedByDescending { it.source_order }
it.reversed()
} else {
it
}
}
// SY <--
return if (presenter.sortDescending()) {
chapters.reversed()
} else {
chapters
}
}
private fun downloadChapters(choice: Int) { private fun downloadChapters(choice: Int) {
val chaptersToDownload = when (choice) { val chaptersToDownload = when (choice) {
@@ -846,11 +846,7 @@ class MangaPresenter(
} }
// SY <-- // SY <--
return observable.toSortedList(getChapterSort()) val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) {
}
fun getChapterSort(): (Chapter, Chapter) -> Int {
return when (manga.sorting) {
Manga.SORTING_SOURCE -> when (sortDescending()) { Manga.SORTING_SOURCE -> when (sortDescending()) {
true -> { c1, c2 -> c1.source_order.compareTo(c2.source_order) } true -> { c1, c2 -> c1.source_order.compareTo(c2.source_order) }
false -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) } false -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) }
@@ -865,6 +861,8 @@ class MangaPresenter(
} }
else -> throw NotImplementedError("Unimplemented sorting method") else -> throw NotImplementedError("Unimplemented sorting method")
} }
return observable.toSortedList(sortFunction)
} }
/** /**
@@ -891,19 +889,11 @@ class MangaPresenter(
* Returns the next unread chapter or null if everything is read. * Returns the next unread chapter or null if everything is read.
*/ */
fun getNextUnreadChapter(): ChapterItem? { fun getNextUnreadChapter(): ChapterItem? {
val chapters = chapters.sortedWith(getChapterSort())
return if (source.isEhBasedSource()) { return if (source.isEhBasedSource()) {
if (sortDescending()) { val chapter = chapters.sortedBy { it.source_order }.getOrNull(0)
chapters.firstOrNull()?.takeUnless { it.read } if (chapter?.read == false) chapter else null
} else {
chapters.lastOrNull()?.takeUnless { it.read }
}
} else { } else {
if (sortDescending()) { chapters.sortedByDescending { it.source_order }.find { !it.read }
return chapters.findLast { !it.read }
} else {
chapters.find { !it.read }
}
} }
} }
@@ -6,38 +6,42 @@ import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.databinding.ChapterDownloadViewBinding import eu.kanade.tachiyomi.databinding.ChapterDownloadViewBinding
import eu.kanade.tachiyomi.util.view.setVectorCompat
class ChapterDownloadView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : class ChapterDownloadView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
FrameLayout(context, attrs) { FrameLayout(context, attrs) {
private val binding: ChapterDownloadViewBinding = private val binding: ChapterDownloadViewBinding
ChapterDownloadViewBinding.inflate(LayoutInflater.from(context), this, false)
private var state = Download.State.NOT_DOWNLOADED private var state = Download.State.NOT_DOWNLOADED
private var progress = 0 private var progress = 0
private var downloadIconAnimator: ObjectAnimator? = null private var downloadIconAnimator: ObjectAnimator? = null
private var isAnimating = false
init { init {
binding = ChapterDownloadViewBinding.inflate(LayoutInflater.from(context), this, false)
addView(binding.root) addView(binding.root)
} }
fun setState(state: Download.State, progress: Int = 0) { fun setState(state: Download.State, progress: Int = 0) {
val isDirty = this.state.value != state.value || this.progress != progress val isDirty = this.state.value != state.value || this.progress != progress
this.state = state
this.progress = progress
if (isDirty) { if (isDirty) {
updateLayout(state, progress) updateLayout()
} }
} }
private fun updateLayout(state: Download.State, progress: Int) { private fun updateLayout() {
binding.downloadIcon.isVisible = state == Download.State.NOT_DOWNLOADED || binding.downloadIconBorder.isVisible = state == Download.State.NOT_DOWNLOADED
state == Download.State.DOWNLOADING || state == Download.State.QUEUE
if (state == Download.State.DOWNLOADING || state == Download.State.QUEUE) { binding.downloadIcon.isVisible = state == Download.State.NOT_DOWNLOADED || state == Download.State.DOWNLOADING
if (downloadIconAnimator == null) { if (state == Download.State.DOWNLOADING) {
if (!isAnimating) {
downloadIconAnimator = downloadIconAnimator =
ObjectAnimator.ofFloat(binding.downloadIcon, "alpha", 1f, 0f).apply { ObjectAnimator.ofFloat(binding.downloadIcon, "alpha", 1f, 0f).apply {
duration = 1000 duration = 1000
@@ -45,36 +49,22 @@ class ChapterDownloadView @JvmOverloads constructor(context: Context, attrs: Att
repeatMode = ObjectAnimator.REVERSE repeatMode = ObjectAnimator.REVERSE
} }
downloadIconAnimator?.start() downloadIconAnimator?.start()
isAnimating = true
} }
downloadIconAnimator?.currentPlayTime = System.currentTimeMillis() % 2000 } else {
} else if (downloadIconAnimator != null) {
downloadIconAnimator?.cancel() downloadIconAnimator?.cancel()
downloadIconAnimator = null
binding.downloadIcon.alpha = 1f binding.downloadIcon.alpha = 1f
isAnimating = false
} }
binding.downloadQueued.isVisible = state == Download.State.QUEUE
binding.downloadProgress.isVisible = state == Download.State.DOWNLOADING || binding.downloadProgress.isVisible = state == Download.State.DOWNLOADING ||
state == Download.State.NOT_DOWNLOADED || state == Download.State.QUEUE (state == Download.State.QUEUE && progress > 0)
if (state == Download.State.DOWNLOADING) { binding.downloadProgress.progress = progress
binding.downloadProgress.setProgressCompat(progress, true)
} else {
binding.downloadProgress.setProgressCompat(100, true)
}
binding.downloadStatusIcon.apply { binding.downloadedIcon.isVisible = state == Download.State.DOWNLOADED
if (state == Download.State.DOWNLOADED || state == Download.State.ERROR) {
isVisible = true
if (state == Download.State.DOWNLOADED) {
setVectorCompat(R.drawable.ic_check_circle_24dp, android.R.attr.textColorPrimary)
} else {
setVectorCompat(R.drawable.ic_error_outline_24dp, R.attr.colorError)
}
} else {
isVisible = false
}
}
this.state = state binding.errorIcon.isVisible = state == Download.State.ERROR
this.progress = progress
} }
} }
@@ -1,9 +1,9 @@
package eu.kanade.tachiyomi.ui.manga.chapter package eu.kanade.tachiyomi.ui.manga.chapter
import android.text.SpannableString
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan
import android.view.View import android.view.View
import androidx.core.text.buildSpannedString
import androidx.core.text.color
import androidx.core.view.isVisible import androidx.core.view.isVisible
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
@@ -64,10 +64,8 @@ class ChapterHolder(
} else /* SY <-- */ descriptions.add(adapter.dateFormat.format(Date(chapter.date_upload))) } else /* SY <-- */ descriptions.add(adapter.dateFormat.format(Date(chapter.date_upload)))
} }
if ((!chapter.read || (adapter.preserveReadingPosition && manga.isEhBasedManga())) && chapter.last_page_read > 0) { if ((!chapter.read || (adapter.preserveReadingPosition && manga.isEhBasedManga())) && chapter.last_page_read > 0) {
val lastPageRead = buildSpannedString { val lastPageRead = SpannableString(itemView.context.getString(R.string.chapter_progress, chapter.last_page_read + 1)).apply {
color(adapter.readColor) { setSpan(ForegroundColorSpan(adapter.readColor), 0, length, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE)
append(itemView.context.getString(R.string.chapter_progress, chapter.last_page_read + 1))
}
} }
descriptions.add(lastPageRead) descriptions.add(lastPageRead)
} }
@@ -61,12 +61,16 @@ class ChaptersSettingsSheet(
private fun showPopupMenu(view: View) { private fun showPopupMenu(view: View) {
view.popupMenu( view.popupMenu(
menuRes = R.menu.default_chapter_filter, R.menu.default_chapter_filter,
onMenuItemClick = { {
when (itemId) { },
{
when (this.itemId) {
R.id.set_as_default -> { R.id.set_as_default -> {
SetChapterSettingsDialog(presenter.manga).showDialog(router) SetChapterSettingsDialog(presenter.manga).showDialog(router)
true
} }
else -> true
} }
} }
) )
@@ -29,6 +29,7 @@ open class BaseChapterHolder(
}, },
onMenuItemClick = { onMenuItemClick = {
adapter.clickListener.deleteChapter(position) adapter.clickListener.deleteChapter(position)
true
} }
) )
} }
@@ -85,13 +85,7 @@ class EditMergedSettingsHeaderAdapter(private val controller: EditMergedSettings
val mergedMangas = controller.mergedMangas val mergedMangas = controller.mergedMangas
val mangaInfoAdapter: ArrayAdapter<String> = ArrayAdapter( val mangaInfoAdapter: ArrayAdapter<String> = ArrayAdapter(itemView.context, android.R.layout.simple_spinner_item, mergedMangas.map { sourceManager.getOrStub(it.second.mangaSourceId).toString() + " " + it.first?.title })
itemView.context,
android.R.layout.simple_spinner_item,
mergedMangas.map {
sourceManager.getOrStub(it.second.mangaSourceId).toString() + " " + it.first?.title
}
)
mangaInfoAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) mangaInfoAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
binding.mangaInfoSpinner.adapter = mangaInfoAdapter binding.mangaInfoSpinner.adapter = mangaInfoAdapter
@@ -108,16 +102,11 @@ class EditMergedSettingsHeaderAdapter(private val controller: EditMergedSettings
position: Int, position: Int,
id: Long id: Long
) { ) {
val mergedInfoManga = controller.mergedMangas controller.mergedMangas.find { mergedManga -> mergedManga.second.id == mergedMangas.getOrNull(position)?.second?.id }?.second?.let { newInfoManga ->
.find { mergedManga ->
mergedManga.second.id == mergedMangas.getOrNull(position)?.second?.id
}
if (mergedInfoManga != null) {
controller.mergedMangas.onEach { controller.mergedMangas.onEach {
it.second.isInfoManga = false it.second.isInfoManga = false
} }
mergedInfoManga.second.isInfoManga = true newInfoManga.isInfoManga = true
} }
} }
@@ -16,7 +16,6 @@ import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.openInBrowser import eu.kanade.tachiyomi.ui.base.controller.openInBrowser
import eu.kanade.tachiyomi.ui.main.WhatsNewDialogController import eu.kanade.tachiyomi.ui.main.WhatsNewDialogController
import eu.kanade.tachiyomi.ui.setting.SettingsController import eu.kanade.tachiyomi.ui.setting.SettingsController
import eu.kanade.tachiyomi.util.CrashLogUtil
import eu.kanade.tachiyomi.util.lang.launchNow import eu.kanade.tachiyomi.util.lang.launchNow
import eu.kanade.tachiyomi.util.lang.toDateTimestampString import eu.kanade.tachiyomi.util.lang.toDateTimestampString
import eu.kanade.tachiyomi.util.preference.onClick import eu.kanade.tachiyomi.util.preference.onClick
@@ -88,14 +87,6 @@ class AboutController : SettingsController() {
onClick { openInBrowser(it) } onClick { openInBrowser(it) }
} }
} }
preference {
key = "pref_about_facebook"
title = "Facebook"
"https://facebook.com/tachiyomiorg".also {
summary = it
onClick { openInBrowser(it) }
}
}
preference { preference {
key = "pref_about_twitter" key = "pref_about_twitter"
title = "Twitter" title = "Twitter"
@@ -126,7 +117,7 @@ class AboutController : SettingsController() {
preference { preference {
key = "pref_about_label_original_tachiyomi_github" key = "pref_about_label_original_tachiyomi_github"
title = "Original Tachiyomi GitHub " title = "Original Tachiyomi GitHub "
"https://github.com/tachiyomiorg".also { "https://github.com/tachiyomiorg/tachiyomi".also {
summary = it summary = it
onClick { openInBrowser(it) } onClick { openInBrowser(it) }
} }
@@ -149,7 +140,6 @@ class AboutController : SettingsController() {
.withAboutIconShown(false) .withAboutIconShown(false)
.withAboutVersionShown(false) .withAboutVersionShown(false)
.withLicenseShown(true) .withLicenseShown(true)
.withEdgeToEdge(true)
.start(activity!!) .start(activity!!)
} }
} }
@@ -162,11 +152,6 @@ class AboutController : SettingsController() {
private fun checkVersion() { private fun checkVersion() {
if (activity == null) return if (activity == null) return
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
activity?.toast(R.string.update_check_eol)
return
}
activity?.toast(R.string.update_check_look_for_updates) activity?.toast(R.string.update_check_look_for_updates)
launchNow { launchNow {
@@ -218,10 +203,20 @@ class AboutController : SettingsController() {
} }
private fun copyDebugInfo() { private fun copyDebugInfo() {
activity?.let { val deviceInfo =
val deviceInfo = CrashLogUtil(it).getDebugInfo() """
activity?.copyToClipboard("Debug information", deviceInfo) App version: ${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}, ${BuildConfig.COMMIT_SHA}, ${BuildConfig.VERSION_CODE})
} Preview build: $syDebugVersion
Android version: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT})
Android build ID: ${Build.DISPLAY}
Device brand: ${Build.BRAND}
Device manufacturer: ${Build.MANUFACTURER}
Device name: ${Build.DEVICE}
Device model: ${Build.MODEL}
Device product name: ${Build.PRODUCT}
""".trimIndent()
activity?.copyToClipboard("Debug information", deviceInfo)
} }
private fun getFormattedBuildTime(): String { private fun getFormattedBuildTime(): String {
@@ -1,11 +1,7 @@
package eu.kanade.tachiyomi.ui.more package eu.kanade.tachiyomi.ui.more
import android.content.Context import android.content.Context
import android.os.Bundle
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@@ -31,10 +27,7 @@ import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.openInBrowser
import exh.ui.batchadd.BatchAddController import exh.ui.batchadd.BatchAddController
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.subscriptions.CompositeSubscription
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
@@ -47,9 +40,6 @@ class MoreController :
private var isDownloading: Boolean = false private var isDownloading: Boolean = false
private var downloadQueueSize: Int = 0 private var downloadQueueSize: Int = 0
private var untilDestroySubscriptions = CompositeSubscription()
private set
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.label_more titleRes = R.string.label_more
@@ -138,19 +128,6 @@ class MoreController :
} }
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle?): View {
if (untilDestroySubscriptions.isUnsubscribed) {
untilDestroySubscriptions = CompositeSubscription()
}
return super.onCreateView(inflater, container, savedInstanceState)
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
untilDestroySubscriptions.unsubscribe()
}
private fun initDownloadQueueSummary(preference: Preference) { private fun initDownloadQueueSummary(preference: Preference) {
// Handle running/paused status change // Handle running/paused status change
DownloadService.runningRelay DownloadService.runningRelay
@@ -177,10 +154,6 @@ class MoreController :
} }
} }
private fun <T> Observable<T>.subscribeUntilDestroy(onNext: (T) -> Unit): Subscription {
return subscribe(onNext).also { untilDestroySubscriptions.add(it) }
}
private class MoreHeaderPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : private class MoreHeaderPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
Preference(context, attrs) { Preference(context, attrs) {
@@ -11,6 +11,7 @@ import android.graphics.Bitmap
import android.graphics.Color import android.graphics.Color
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.Gravity
import android.view.KeyEvent import android.view.KeyEvent
import android.view.Menu import android.view.Menu
import android.view.MotionEvent import android.view.MotionEvent
@@ -67,7 +68,6 @@ import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.defaultBar import eu.kanade.tachiyomi.util.view.defaultBar
import eu.kanade.tachiyomi.util.view.hideBar import eu.kanade.tachiyomi.util.view.hideBar
import eu.kanade.tachiyomi.util.view.isDefaultBar import eu.kanade.tachiyomi.util.view.isDefaultBar
import eu.kanade.tachiyomi.util.view.popupMenu
import eu.kanade.tachiyomi.util.view.setTooltip import eu.kanade.tachiyomi.util.view.setTooltip
import eu.kanade.tachiyomi.util.view.showBar import eu.kanade.tachiyomi.util.view.showBar
import eu.kanade.tachiyomi.widget.SimpleAnimationListener import eu.kanade.tachiyomi.widget.SimpleAnimationListener
@@ -458,18 +458,13 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
setTooltip(R.string.viewer) setTooltip(R.string.viewer)
setOnClickListener { setOnClickListener {
popupMenu( val newReadingMode =
items = ReadingModeType.values().map { it.prefValue to it.stringRes }, ReadingModeType.getNextReadingMode(presenter.getMangaViewer(resolveDefault = false))
selectedItemId = presenter.getMangaViewer(resolveDefault = false), presenter.setMangaViewer(newReadingMode.prefValue)
) {
val newReadingMode = ReadingModeType.fromPreference(itemId)
presenter.setMangaViewer(newReadingMode.prefValue) menuToggleToast?.cancel()
if (!preferences.showReadingMode()) {
menuToggleToast?.cancel() menuToggleToast = toast(newReadingMode.stringRes)
if (!preferences.showReadingMode()) {
menuToggleToast = toast(newReadingMode.stringRes)
}
} }
} }
} }
@@ -479,18 +474,14 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
setTooltip(R.string.pref_rotation_type) setTooltip(R.string.pref_rotation_type)
setOnClickListener { setOnClickListener {
popupMenu( val newOrientation =
items = OrientationType.values().map { it.prefValue to it.stringRes }, OrientationType.getNextOrientation(preferences.rotation().get(), resources)
selectedItemId = preferences.rotation().get(),
) {
val newOrientation = OrientationType.fromPreference(itemId)
preferences.rotation().set(newOrientation.prefValue) preferences.rotation().set(newOrientation.prefValue)
setOrientation(newOrientation.flag) setOrientation(newOrientation.flag)
menuToggleToast?.cancel() menuToggleToast?.cancel()
menuToggleToast = toast(newOrientation.stringRes) menuToggleToast = toast(newOrientation.stringRes)
}
} }
} }
preferences.rotation().asImmediateFlow { updateRotationShortcut(it) } preferences.rotation().asImmediateFlow { updateRotationShortcut(it) }
@@ -532,11 +523,6 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
setOnClickListener { setOnClickListener {
ReaderSettingsSheet(this@ReaderActivity).show() ReaderSettingsSheet(this@ReaderActivity).show()
} }
setOnLongClickListener {
ReaderSettingsSheet(this@ReaderActivity, showColorFilterSettings = true).show()
true
}
} }
// --> EH // --> EH
@@ -732,7 +718,7 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
// EXH <-- // EXH <--
/*private fun updateRotationShortcut(preference: Int) { /*private fun updateRotationShortcut(preference: Int) {
val orientation = OrientationType.fromPreference(preference) val orientation = OrientationType.fromPreference(preference, resources)
binding.actionRotation.setImageResource(orientation.iconRes) binding.actionRotation.setImageResource(orientation.iconRes)
}*/ }*/
@@ -899,10 +885,12 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
binding.viewerContainer.addView(newViewer.getView()) binding.viewerContainer.addView(newViewer.getView())
// SY --> // SY -->
val defaultReaderType = manga.defaultReaderType(manga.mangaType(sourceName = sourceManager.get(manga.source)?.name)) val defaultReaderType = manga.defaultReaderType(manga.mangaType(sourceName = sourceManager.getOrStub(manga.source).name))
if (preferences.useAutoWebtoon().get() && manga.viewer == 0 && defaultReaderType != null && defaultReaderType == ReadingModeType.WEBTOON.prefValue) { if (preferences.useAutoWebtoon().get() && manga.viewer == 0 && defaultReaderType != null && defaultReaderType == ReadingModeType.WEBTOON.prefValue) {
readingModeToast?.cancel() readingModeToast?.cancel()
readingModeToast = toast(resources.getString(R.string.eh_auto_webtoon_snack)) readingModeToast = this.toast(resources.getString(R.string.eh_auto_webtoon_snack)) {
it.setGravity(Gravity.CENTER_VERTICAL or Gravity.CENTER_HORIZONTAL, 0, 0)
}
} else if (preferences.showReadingMode()) { } else if (preferences.showReadingMode()) {
// SY <-- // SY <--
showReadingModeToast(presenter.getMangaViewer()) showReadingModeToast(presenter.getMangaViewer())
@@ -958,12 +946,10 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
} }
private fun showReadingModeToast(mode: Int) { private fun showReadingModeToast(mode: Int) {
try { val strings = resources.getStringArray(R.array.viewers_selector)
val strings = resources.getStringArray(R.array.viewers_selector) readingModeToast?.cancel()
readingModeToast?.cancel() readingModeToast = toast(strings[mode]) {
readingModeToast = toast(strings[mode]) it.setGravity(Gravity.CENTER_VERTICAL or Gravity.CENTER_HORIZONTAL, 0, 0)
} catch (e: ArrayIndexOutOfBoundsException) {
Timber.e("Unknown reading mode: $mode")
} }
} }
@@ -1192,7 +1178,7 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
* Forces the user preferred [orientation] on the activity. * Forces the user preferred [orientation] on the activity.
*/ */
private fun setOrientation(orientation: Int) { private fun setOrientation(orientation: Int) {
val newOrientation = OrientationType.fromPreference(orientation) val newOrientation = OrientationType.fromPreference(orientation, resources)
if (newOrientation.flag != requestedOrientation) { if (newOrientation.flag != requestedOrientation) {
requestedOrientation = newOrientation.flag requestedOrientation = newOrientation.flag
} }
@@ -598,7 +598,7 @@ class ReaderPresenter(
val manga = manga ?: return preferences.defaultViewer() val manga = manga ?: return preferences.defaultViewer()
// SY --> // SY -->
return if (resolveDefault && manga.viewer == 0 && preferences.useAutoWebtoon().get()) { return if (resolveDefault && manga.viewer == 0 && preferences.useAutoWebtoon().get()) {
manga.defaultReaderType(manga.mangaType(sourceName = sourceManager.get(manga.source)?.name)) ?: if (manga.viewer == 0) preferences.defaultViewer() else manga.viewer manga.defaultReaderType(manga.mangaType(sourceName = sourceManager.getOrStub(manga.source).name)) ?: if (manga.viewer == 0) preferences.defaultViewer() else manga.viewer
} else if (resolveDefault && manga.viewer == 0) { } else if (resolveDefault && manga.viewer == 0) {
preferences.defaultViewer() preferences.defaultViewer()
} else { } else {
@@ -99,7 +99,8 @@ class HttpPageLoader(
* the local cache, otherwise fallbacks to network. * the local cache, otherwise fallbacks to network.
*/ */
override fun getPages(): Observable<List<ReaderPage>> { override fun getPages(): Observable<List<ReaderPage>> {
return Observable.fromCallable { chapterCache.getPageListFromCache(chapter.chapter) } return chapterCache
.getPageListFromCache(chapter.chapter)
.onErrorResumeNext { source.fetchPageList(chapter.chapter) } .onErrorResumeNext { source.fetchPageList(chapter.chapter) }
.map { pages -> .map { pages ->
// SY --> // SY -->
@@ -1,20 +1,43 @@
package eu.kanade.tachiyomi.ui.reader.setting package eu.kanade.tachiyomi.ui.reader.setting
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.content.res.Resources
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.lang.next
enum class OrientationType(val prefValue: Int, val flag: Int, @StringRes val stringRes: Int, @DrawableRes val iconRes: Int) { enum class OrientationType(val prefValue: Int, val flag: Int, @StringRes val stringRes: Int, @DrawableRes val iconRes: Int) {
FREE(1, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, R.string.rotation_free, R.drawable.ic_screen_rotation_24dp), FREE(1, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, R.string.rotation_free, R.drawable.ic_screen_rotation_24dp),
PORTRAIT(2, ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT, R.string.rotation_portrait, R.drawable.ic_stay_current_portrait_24dp), LOCKED_PORTRAIT(2, ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT, R.string.rotation_lock, R.drawable.ic_screen_lock_rotation_24dp),
LANDSCAPE(3, ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE, R.string.rotation_landscape, R.drawable.ic_stay_current_landscape_24dp), LOCKED_LANDSCAPE(2, ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE, R.string.rotation_lock, R.drawable.ic_screen_lock_rotation_24dp),
LOCKED_PORTRAIT(4, ActivityInfo.SCREEN_ORIENTATION_PORTRAIT, R.string.rotation_force_portrait, R.drawable.ic_screen_lock_portrait_24dp), PORTRAIT(3, ActivityInfo.SCREEN_ORIENTATION_PORTRAIT, R.string.rotation_force_portrait, R.drawable.ic_screen_lock_portrait_24dp),
LOCKED_LANDSCAPE(5, ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE, R.string.rotation_force_landscape, R.drawable.ic_screen_lock_landscape_24dp), LANDSCAPE(4, ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE, R.string.rotation_force_landscape, R.drawable.ic_screen_lock_landscape_24dp);
;
companion object { companion object {
fun fromPreference(preference: Int): OrientationType = fun fromPreference(preference: Int, resources: Resources): OrientationType = when (preference) {
values().find { it.prefValue == preference } ?: FREE 2 -> {
val currentOrientation = resources.configuration.orientation
if (currentOrientation == Configuration.ORIENTATION_PORTRAIT) {
LOCKED_PORTRAIT
} else {
LOCKED_LANDSCAPE
}
}
3 -> PORTRAIT
4 -> LANDSCAPE
else -> FREE
}
fun getNextOrientation(preference: Int, resources: Resources): OrientationType {
val current = if (preference == 2) {
// Avoid issue due to 2 types having the same prefValue
LOCKED_LANDSCAPE
} else {
fromPreference(preference, resources)
}
return current.next()
}
} }
} }
@@ -8,10 +8,7 @@ import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.widget.SimpleTabSelectedListener import eu.kanade.tachiyomi.widget.SimpleTabSelectedListener
import eu.kanade.tachiyomi.widget.sheet.TabbedBottomSheetDialog import eu.kanade.tachiyomi.widget.sheet.TabbedBottomSheetDialog
class ReaderSettingsSheet( class ReaderSettingsSheet(private val activity: ReaderActivity) : TabbedBottomSheetDialog(activity) {
private val activity: ReaderActivity,
showColorFilterSettings: Boolean = false,
) : TabbedBottomSheetDialog(activity) {
private val readingModeSettings = ReaderReadingModeSettings(activity) private val readingModeSettings = ReaderReadingModeSettings(activity)
private val generalSettings = ReaderGeneralSettings(activity) private val generalSettings = ReaderGeneralSettings(activity)
@@ -22,7 +19,7 @@ class ReaderSettingsSheet(
init { init {
val sheetBehavior = BottomSheetBehavior.from(binding.root.parent as ViewGroup) val sheetBehavior = BottomSheetBehavior.from(binding.root.parent as ViewGroup)
sheetBehavior.isFitToContents = false sheetBehavior.isFitToContents = false
sheetBehavior.halfExpandedRatio = 0.25f sheetBehavior.halfExpandedRatio = 0.5f
val filterTabIndex = getTabViews().indexOf(colorFilterSettings) val filterTabIndex = getTabViews().indexOf(colorFilterSettings)
binding.tabs.addOnTabSelectedListener(object : SimpleTabSelectedListener() { binding.tabs.addOnTabSelectedListener(object : SimpleTabSelectedListener() {
@@ -36,12 +33,13 @@ class ReaderSettingsSheet(
if (activity.menuVisible != !isFilterTab) { if (activity.menuVisible != !isFilterTab) {
activity.setMenuVisibility(!isFilterTab) activity.setMenuVisibility(!isFilterTab)
} }
// Partially collapse the sheet for better preview
if (isFilterTab) {
sheetBehavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
}
} }
}) })
if (showColorFilterSettings) {
binding.tabs.getTabAt(filterTabIndex)?.select()
}
} }
override fun getTabViews() = listOf( override fun getTabViews() = listOf(
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.reader.setting
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.lang.next
enum class ReadingModeType(val prefValue: Int, @StringRes val stringRes: Int, @DrawableRes val iconRes: Int) { enum class ReadingModeType(val prefValue: Int, @StringRes val stringRes: Int, @DrawableRes val iconRes: Int) {
DEFAULT(0, R.string.default_viewer, R.drawable.ic_reader_default_24dp), DEFAULT(0, R.string.default_viewer, R.drawable.ic_reader_default_24dp),
@@ -16,6 +17,11 @@ enum class ReadingModeType(val prefValue: Int, @StringRes val stringRes: Int, @D
companion object { companion object {
fun fromPreference(preference: Int): ReadingModeType = values().find { it.prefValue == preference } ?: DEFAULT fun fromPreference(preference: Int): ReadingModeType = values().find { it.prefValue == preference } ?: DEFAULT
fun getNextReadingMode(preference: Int): ReadingModeType {
val current = fromPreference(preference)
return current.next()
}
fun isPagerType(preference: Int): Boolean { fun isPagerType(preference: Int): Boolean {
val mode = fromPreference(preference) val mode = fromPreference(preference)
return mode == LEFT_TO_RIGHT || mode == RIGHT_TO_LEFT || mode == VERTICAL return mode == LEFT_TO_RIGHT || mode == RIGHT_TO_LEFT || mode == VERTICAL
@@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.widget package eu.kanade.tachiyomi.ui.reader.setting
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.Gravity import android.view.Gravity
@@ -8,21 +7,15 @@ import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.annotation.ArrayRes import androidx.annotation.ArrayRes
import androidx.appcompat.view.menu.MenuBuilder
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.core.content.ContextCompat
import androidx.core.view.forEach
import androidx.core.view.get
import com.tfcporciuncula.flow.Preference import com.tfcporciuncula.flow.Preference
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.SpinnerPreferenceBinding import eu.kanade.tachiyomi.databinding.SpinnerPreferenceBinding
import eu.kanade.tachiyomi.util.system.getResourceColor
class MaterialSpinnerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : class SpinnerPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
FrameLayout(context, attrs) { FrameLayout(context, attrs) {
private var entries = emptyList<String>() private var entries = emptyList<String>()
private var selectedPosition = 0
private var popup: PopupMenu? = null private var popup: PopupMenu? = null
var onItemSelectedListener: ((Int) -> Unit)? = null var onItemSelectedListener: ((Int) -> Unit)? = null
@@ -37,26 +30,17 @@ class MaterialSpinnerView @JvmOverloads constructor(context: Context, attrs: Att
} }
} }
private val emptyIcon by lazy {
ContextCompat.getDrawable(context, R.drawable.ic_blank_24dp)
}
private val checkmarkIcon by lazy {
ContextCompat.getDrawable(context, R.drawable.ic_check_24dp)?.mutate()?.apply {
setTint(context.getResourceColor(android.R.attr.textColorPrimary))
}
}
private val binding = SpinnerPreferenceBinding.inflate(LayoutInflater.from(context), this, false) private val binding = SpinnerPreferenceBinding.inflate(LayoutInflater.from(context), this, false)
init { init {
addView(binding.root) addView(binding.root)
val attr = context.obtainStyledAttributes(attrs, R.styleable.MaterialSpinnerView) val attr = context.obtainStyledAttributes(attrs, R.styleable.SpinnerPreference)
val title = attr.getString(R.styleable.MaterialSpinnerView_title).orEmpty() val title = attr.getString(R.styleable.SpinnerPreference_title).orEmpty()
binding.title.text = title binding.title.text = title
val entries = (attr.getTextArray(R.styleable.MaterialSpinnerView_android_entries) ?: emptyArray()).map { it.toString() } val entries = (attr.getTextArray(R.styleable.SpinnerPreference_android_entries) ?: emptyArray()).map { it.toString() }
this.entries = entries this.entries = entries
binding.details.text = entries.firstOrNull().orEmpty() binding.details.text = entries.firstOrNull().orEmpty()
@@ -64,14 +48,6 @@ class MaterialSpinnerView @JvmOverloads constructor(context: Context, attrs: Att
} }
fun setSelection(selection: Int) { fun setSelection(selection: Int) {
popup?.menu?.get(selectedPosition)?.let {
it.icon = emptyIcon
it.title = entries[selectedPosition]
}
selectedPosition = selection
popup?.menu?.get(selectedPosition)?.let {
it.icon = checkmarkIcon
}
binding.details.text = entries.getOrNull(selection).orEmpty() binding.details.text = entries.getOrNull(selection).orEmpty()
} }
@@ -142,19 +118,11 @@ class MaterialSpinnerView @JvmOverloads constructor(context: Context, attrs: Att
return pos return pos
} }
@SuppressLint("RestrictedApi")
fun createPopupMenu(onItemClick: (Int) -> Unit): PopupMenu { fun createPopupMenu(onItemClick: (Int) -> Unit): PopupMenu {
val popup = PopupMenu(context, this, Gravity.END, R.attr.actionOverflowMenuStyle, 0) val popup = PopupMenu(context, this, Gravity.END, R.attr.actionOverflowMenuStyle, 0)
entries.forEachIndexed { index, entry -> entries.forEachIndexed { index, entry ->
popup.menu.add(0, index, 0, entry) popup.menu.add(0, index, 0, entry)
} }
(popup.menu as? MenuBuilder)?.setOptionalIconsVisible(true)
popup.menu.forEach {
it.icon = emptyIcon
}
popup.menu.getItem(selectedPosition)?.let {
it.icon = checkmarkIcon
}
popup.setOnMenuItemClickListener { menuItem -> popup.setOnMenuItemClickListener { menuItem ->
val pos = menuClicked(menuItem) val pos = menuClicked(menuItem)
onItemClick(pos) onItemClick(pos)
@@ -250,13 +250,16 @@ class PagerPageHolder(
readImageHeaderSubscription = Observable readImageHeaderSubscription = Observable
.fromCallable { .fromCallable {
val stream = streamFn().buffered(16) val stream = streamFn().buffered(16)
openStream = process(stream) openStream = stream
ImageUtil.findImageType(stream) == ImageUtil.ImageType.GIF ImageUtil.findImageType(stream) == ImageUtil.ImageType.GIF
} }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.doOnNext { isAnimated -> .doOnNext { isAnimated ->
if (viewer.config.dualPageSplit) {
openStream = processDualPageSplit(openStream!!)
}
if (!isAnimated) { if (!isAnimated) {
// SY --> // SY -->
if (readerTheme >= 3) { if (readerTheme >= 3) {
@@ -292,31 +295,21 @@ class PagerPageHolder(
.subscribe({}, {}) .subscribe({}, {})
} }
private fun process(imageStream: InputStream): InputStream { private fun processDualPageSplit(openStream: InputStream): InputStream {
if (!viewer.config.dualPageSplit) { var inputStream = openStream
return imageStream val (isDoublePage, stream) = when (page) {
is InsertPage -> Pair(true, inputStream)
else -> ImageUtil.isDoublePage(inputStream)
} }
inputStream = stream
if (page is InsertPage) { if (!isDoublePage) return inputStream
return splitInHalf(imageStream)
}
val isDoublePage = ImageUtil.isDoublePage(imageStream)
if (!isDoublePage) {
return imageStream
}
onPageSplit()
return splitInHalf(imageStream)
}
private fun splitInHalf(imageStream: InputStream): InputStream {
var side = when { var side = when {
viewer is L2RPagerViewer && page is InsertPage -> ImageUtil.Side.RIGHT viewer is L2RPagerViewer && page is InsertPage -> ImageUtil.Side.RIGHT
viewer !is L2RPagerViewer && page is InsertPage -> ImageUtil.Side.LEFT (viewer is R2LPagerViewer || viewer is VerticalPagerViewer) && page is InsertPage -> ImageUtil.Side.LEFT
viewer is L2RPagerViewer && page !is InsertPage -> ImageUtil.Side.LEFT viewer is L2RPagerViewer && page !is InsertPage -> ImageUtil.Side.LEFT
viewer !is L2RPagerViewer && page !is InsertPage -> ImageUtil.Side.RIGHT (viewer is R2LPagerViewer || viewer is VerticalPagerViewer) && page !is InsertPage -> ImageUtil.Side.RIGHT
else -> error("We should choose a side!") else -> error("We should choose a side!")
} }
@@ -327,7 +320,11 @@ class PagerPageHolder(
} }
} }
return ImageUtil.splitInHalf(imageStream, side) if (page !is InsertPage) {
onPageSplit()
}
return ImageUtil.splitInHalf(inputStream, side)
} }
private fun onPageSplit() { private fun onPageSplit() {
@@ -385,10 +385,7 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
} }
fun onPageSplit(currentPage: ReaderPage, newPage: InsertPage) { fun onPageSplit(currentPage: ReaderPage, newPage: InsertPage) {
activity.runOnUiThread { adapter.onPageSplit(currentPage, newPage, this::class.java)
// Need to insert on UI thread else images will go blank
adapter.onPageSplit(currentPage, newPage, this::class.java)
}
} }
private fun cleanupPageSplit() { private fun cleanupPageSplit() {
@@ -281,13 +281,22 @@ class WebtoonPageHolder(
readImageHeaderSubscription = Observable readImageHeaderSubscription = Observable
.fromCallable { .fromCallable {
val stream = streamFn().buffered(16) val stream = streamFn().buffered(16)
openStream = process(stream) openStream = stream
ImageUtil.findImageType(stream) == ImageUtil.ImageType.GIF ImageUtil.findImageType(stream) == ImageUtil.ImageType.GIF
} }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.doOnNext { isAnimated -> .doOnNext { isAnimated ->
if (viewer.config.dualPageSplit) {
val (isDoublePage, stream) = ImageUtil.isDoublePage(openStream!!)
openStream = if (!isDoublePage) {
stream
} else {
val upperSide = if (viewer.config.dualPageInvert) ImageUtil.Side.LEFT else ImageUtil.Side.RIGHT
ImageUtil.splitAndMerge(stream, upperSide)
}
}
if (!isAnimated) { if (!isAnimated) {
val subsamplingView = initSubsamplingImageView() val subsamplingView = initSubsamplingImageView()
subsamplingView.isVisible = true subsamplingView.isVisible = true
@@ -306,20 +315,6 @@ class WebtoonPageHolder(
addSubscription(readImageHeaderSubscription) addSubscription(readImageHeaderSubscription)
} }
private fun process(imageStream: InputStream): InputStream {
if (!viewer.config.dualPageSplit) {
return imageStream
}
val isDoublePage = ImageUtil.isDoublePage(imageStream)
if (!isDoublePage) {
return imageStream
}
val upperSide = if (viewer.config.dualPageInvert) ImageUtil.Side.LEFT else ImageUtil.Side.RIGHT
return ImageUtil.splitAndMerge(imageStream, upperSide)
}
/** /**
* Called when the page has an error. * Called when the page has an error.
*/ */
@@ -79,7 +79,16 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
recycler.addOnScrollListener( recycler.addOnScrollListener(
object : RecyclerView.OnScrollListener() { object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
onScrolled() val position = layoutManager.findLastEndVisibleItemPosition()
val item = adapter.items.getOrNull(position)
val allowPreload = checkAllowPreload(item as? ReaderPage)
if (item != null && currentPage != item) {
currentPage = item
when (item) {
is ReaderPage -> onPageSelected(item, allowPreload)
is ChapterTransition -> onTransitionSelected(item)
}
}
if (dy < 0) { if (dy < 0) {
val firstIndex = layoutManager.findFirstVisibleItemPosition() val firstIndex = layoutManager.findFirstVisibleItemPosition()
@@ -240,27 +249,11 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
val position = adapter.items.indexOf(page) val position = adapter.items.indexOf(page)
if (position != -1) { if (position != -1) {
recycler.scrollToPosition(position) recycler.scrollToPosition(position)
if (layoutManager.findLastEndVisibleItemPosition() == -1) {
onScrolled(position)
}
} else { } else {
Timber.d("Page $page not found in adapter") Timber.d("Page $page not found in adapter")
} }
} }
fun onScrolled(pos: Int? = null) {
val position = pos ?: layoutManager.findLastEndVisibleItemPosition()
val item = adapter.items.getOrNull(position)
val allowPreload = checkAllowPreload(item as? ReaderPage)
if (item != null && currentPage != item) {
currentPage = item
when (item) {
is ReaderPage -> onPageSelected(item, allowPreload)
is ChapterTransition -> onTransitionSelected(item)
}
}
}
/** /**
* Scrolls up by [scrollDistance]. * Scrolls up by [scrollDistance].
*/ */
@@ -8,13 +8,13 @@ import eu.davidea.flexibleadapter.items.AbstractHeaderItem
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.SectionHeaderItemBinding import eu.kanade.tachiyomi.databinding.RecentSectionItemBinding
import java.util.Date import java.util.Date
class DateSectionItem(val date: Date) : AbstractHeaderItem<DateSectionItem.DateSectionItemHolder>() { class DateSectionItem(val date: Date) : AbstractHeaderItem<DateSectionItem.DateSectionItemHolder>() {
override fun getLayoutRes(): Int { override fun getLayoutRes(): Int {
return R.layout.section_header_item return R.layout.recent_section_item
} }
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): DateSectionItemHolder { override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): DateSectionItemHolder {
@@ -39,12 +39,12 @@ class DateSectionItem(val date: Date) : AbstractHeaderItem<DateSectionItem.DateS
inner class DateSectionItemHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter, true) { inner class DateSectionItemHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter, true) {
private val binding = SectionHeaderItemBinding.bind(view) private val binding = RecentSectionItemBinding.bind(view)
private val now = Date().time private val now = Date().time
fun bind(item: DateSectionItem) { fun bind(item: DateSectionItem) {
binding.title.text = DateUtils.getRelativeTimeSpanString(item.date.time, now, DateUtils.DAY_IN_MILLIS) binding.sectionText.text = DateUtils.getRelativeTimeSpanString(item.date.time, now, DateUtils.DAY_IN_MILLIS)
} }
} }
} }
@@ -35,6 +35,7 @@ class HistoryAdapter(controller: HistoryController) :
init { init {
setDisplayHeadersAtStartUp(true) setDisplayHeadersAtStartUp(true)
setStickyHeaders(true)
} }
interface OnResumeClickListener { interface OnResumeClickListener {
@@ -7,6 +7,7 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
@@ -19,6 +20,7 @@ import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.databinding.HistoryControllerBinding import eu.kanade.tachiyomi.databinding.HistoryControllerBinding
import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.RootController import eu.kanade.tachiyomi.ui.base.controller.RootController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
@@ -34,10 +36,13 @@ import uy.kohesive.injekt.injectLazy
/** /**
* Fragment that shows recently read manga. * Fragment that shows recently read manga.
* Uses [R.layout.history_controller].
* UI related actions should be called from here.
*/ */
class HistoryController : class HistoryController :
NucleusController<HistoryControllerBinding, HistoryPresenter>(), NucleusController<HistoryControllerBinding, HistoryPresenter>(),
RootController, RootController,
NoToolbarElevationController,
FlexibleAdapter.OnUpdateListener, FlexibleAdapter.OnUpdateListener,
FlexibleAdapter.EndlessScrollListener, FlexibleAdapter.EndlessScrollListener,
HistoryAdapter.OnRemoveClickListener, HistoryAdapter.OnRemoveClickListener,
@@ -71,16 +76,18 @@ class HistoryController :
return HistoryPresenter() return HistoryPresenter()
} }
override fun createBinding(inflater: LayoutInflater) = HistoryControllerBinding.inflate(inflater) override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = HistoryControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter { binding.recycler.applyInsetter {
type(navigationBars = true) { type(navigationBars = true) {
padding() padding()
} }
} }
return binding.root
}
override fun onViewCreated(view: View) {
super.onViewCreated(view)
// Initialize adapter // Initialize adapter
binding.recycler.layoutManager = LinearLayoutManager(view.context) binding.recycler.layoutManager = LinearLayoutManager(view.context)
@@ -18,6 +18,7 @@ class UpdatesAdapter(
init { init {
setDisplayHeadersAtStartUp(true) setDisplayHeadersAtStartUp(true)
setStickyHeaders(true)
} }
interface OnCoverClickListener { interface OnCoverClickListener {
@@ -5,6 +5,7 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@@ -18,6 +19,7 @@ import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.library.LibraryUpdateService import eu.kanade.tachiyomi.data.library.LibraryUpdateService
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.databinding.UpdatesControllerBinding import eu.kanade.tachiyomi.databinding.UpdatesControllerBinding
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.RootController import eu.kanade.tachiyomi.ui.base.controller.RootController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
@@ -35,10 +37,13 @@ import timber.log.Timber
/** /**
* Fragment that shows recent chapters. * Fragment that shows recent chapters.
* Uses [R.layout.updates_controller].
* UI related actions should be called from here.
*/ */
class UpdatesController : class UpdatesController :
NucleusController<UpdatesControllerBinding, UpdatesPresenter>(), NucleusController<UpdatesControllerBinding, UpdatesPresenter>(),
RootController, RootController,
NoToolbarElevationController,
ActionMode.Callback, ActionMode.Callback,
FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener, FlexibleAdapter.OnItemLongClickListener,
@@ -70,21 +75,18 @@ class UpdatesController :
return UpdatesPresenter() return UpdatesPresenter()
} }
override fun createBinding(inflater: LayoutInflater) = UpdatesControllerBinding.inflate(inflater) override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = UpdatesControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter { binding.recycler.applyInsetter {
type(navigationBars = true) { type(navigationBars = true) {
padding() padding()
} }
} }
binding.actionToolbar.applyInsetter { return binding.root
type(navigationBars = true) { }
margin(bottom = true)
}
}
override fun onViewCreated(view: View) {
super.onViewCreated(view)
view.context.notificationManager.cancel(Notifications.ID_NEW_CHAPTERS) view.context.notificationManager.cancel(Notifications.ID_NEW_CHAPTERS)
// Init RecyclerView and adapter // Init RecyclerView and adapter
@@ -242,7 +244,8 @@ class UpdatesController :
adapter?.currentItems adapter?.currentItems
?.filterIsInstance<UpdatesItem>() ?.filterIsInstance<UpdatesItem>()
?.find { it.chapter.id == download.chapter.id }?.let { ?.find { it.chapter.id == download.chapter.id }?.let {
adapter?.updateItem(it, it.status) adapter?.updateItem(it)
adapter?.notifyDataSetChanged()
} }
} }
@@ -1,19 +1,21 @@
package eu.kanade.tachiyomi.ui.security package eu.kanade.tachiyomi.ui.security
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.BiometricPrompt import androidx.biometric.BiometricPrompt
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.activity.BaseThemedActivity import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.system.BiometricUtil import eu.kanade.tachiyomi.util.system.BiometricUtil
import timber.log.Timber import uy.kohesive.injekt.injectLazy
import java.util.Date import java.util.Date
import java.util.concurrent.Executors import java.util.concurrent.Executors
/** /**
* Blank activity with a BiometricPrompt. * Blank activity with a BiometricPrompt.
*/ */
class BiometricUnlockActivity : BaseThemedActivity() { class BiometricUnlockActivity : AppCompatActivity() {
private val preferences: PreferencesHelper by injectLazy()
private val executor = Executors.newSingleThreadExecutor() private val executor = Executors.newSingleThreadExecutor()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -25,7 +27,6 @@ class BiometricUnlockActivity : BaseThemedActivity() {
object : BiometricPrompt.AuthenticationCallback() { object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString) super.onAuthenticationError(errorCode, errString)
Timber.e(errString.toString())
finishAffinity() finishAffinity()
} }
@@ -29,8 +29,7 @@ import eu.kanade.tachiyomi.source.SourceManager.Companion.DELEGATED_SOURCES
import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.util.CrashLogUtil import eu.kanade.tachiyomi.util.CrashLogUtil
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.preference.defaultValue import eu.kanade.tachiyomi.util.preference.defaultValue
import eu.kanade.tachiyomi.util.preference.editTextPreference import eu.kanade.tachiyomi.util.preference.editTextPreference
import eu.kanade.tachiyomi.util.preference.intListPreference import eu.kanade.tachiyomi.util.preference.intListPreference
@@ -41,7 +40,6 @@ import eu.kanade.tachiyomi.util.preference.preferenceCategory
import eu.kanade.tachiyomi.util.preference.summaryRes import eu.kanade.tachiyomi.util.preference.summaryRes
import eu.kanade.tachiyomi.util.preference.switchPreference import eu.kanade.tachiyomi.util.preference.switchPreference
import eu.kanade.tachiyomi.util.preference.titleRes import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.system.powerManager import eu.kanade.tachiyomi.util.system.powerManager
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import exh.debug.SettingsDebugController import exh.debug.SettingsDebugController
@@ -49,8 +47,15 @@ import exh.log.EHLogLevel
import exh.source.BlacklistedSources import exh.source.BlacklistedSources
import exh.source.EH_SOURCE_ID import exh.source.EH_SOURCE_ID
import exh.source.EXH_SOURCE_ID import exh.source.EXH_SOURCE_ID
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.launch
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@@ -344,32 +349,31 @@ class SettingsAdvancedController : SettingsController() {
private fun cleanupDownloads(removeRead: Boolean, removeNonFavorite: Boolean) { private fun cleanupDownloads(removeRead: Boolean, removeNonFavorite: Boolean) {
if (job?.isActive == true) return if (job?.isActive == true) return
activity?.toast(R.string.starting_cleanup) activity?.toast(R.string.starting_cleanup)
job = launchIO { job = GlobalScope.launch(Dispatchers.IO, CoroutineStart.DEFAULT) {
val mangaList = db.getMangas().executeAsBlocking() val mangaList = db.getMangas().executeAsBlocking()
val sourceManager: SourceManager = Injekt.get()
val downloadManager: DownloadManager = Injekt.get() val downloadManager: DownloadManager = Injekt.get()
var foldersCleared = 0 var foldersCleared = 0
Injekt.get<SourceManager>().getOnlineSources().forEach { source -> val sources = sourceManager.getOnlineSources()
val mangaFolders = downloadManager.getMangaFolders(source)
val sourceManga = mangaList
.asSequence()
.filter { it.source == source.id }
.map { it to DiskUtil.buildValidFilename(it.originalTitle) }
.toList()
mangaFolders.forEach mangaFolder@{ mangaFolder -> for (source in sources) {
val manga = sourceManga.find { (_, folderName) -> folderName == mangaFolder.name }?.first val mangaFolders = downloadManager.getMangaFolders(source)
val sourceManga = mangaList.filter { it.source == source.id }
for (mangaFolder in mangaFolders) {
val manga = sourceManga.find { it.originalTitle == mangaFolder.name }
if (manga == null) { if (manga == null) {
// download is orphaned delete it // download is orphaned delete it
foldersCleared += 1 + (mangaFolder.listFiles().orEmpty().size) foldersCleared += 1 + (mangaFolder.listFiles()?.size ?: 0)
mangaFolder.delete() mangaFolder.delete()
} else { continue
val chapterList = db.getChapters(manga).executeAsBlocking()
foldersCleared += downloadManager.cleanupChapters(chapterList, manga, source, removeRead, removeNonFavorite)
} }
val chapterList = db.getChapters(manga).executeAsBlocking()
foldersCleared += downloadManager.cleanupChapters(chapterList, manga, source, removeRead, removeNonFavorite)
} }
} }
withUIContext { launchUI {
val activity = activity ?: return@withUIContext val activity = activity ?: return@launchUI
val cleanupString = val cleanupString =
if (foldersCleared == 0) activity.getString(R.string.no_folders_to_cleanup) if (foldersCleared == 0) activity.getString(R.string.no_folders_to_cleanup)
else resources!!.getQuantityString( else resources!!.getQuantityString(
@@ -385,18 +389,27 @@ class SettingsAdvancedController : SettingsController() {
private fun clearChapterCache() { private fun clearChapterCache() {
if (activity == null) return if (activity == null) return
launchIO { val files = chapterCache.cacheDir.listFiles() ?: return
try {
val deletedFiles = chapterCache.clear() var deletedFiles = 0
withUIContext {
activity?.toast(resources?.getString(R.string.cache_deleted, deletedFiles)) Observable.defer { Observable.from(files) }
findPreference(CLEAR_CACHE_KEY)?.summary = .doOnNext { file ->
resources?.getString(R.string.used_cache, chapterCache.readableSize) if (chapterCache.removeFileFromCache(file.name)) {
deletedFiles++
} }
} catch (e: Throwable) {
withUIContext { activity?.toast(R.string.cache_delete_error) }
} }
} .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError {
activity?.toast(R.string.cache_delete_error)
}
.doOnCompleted {
activity?.toast(resources?.getString(R.string.cache_deleted, deletedFiles))
findPreference(CLEAR_CACHE_KEY)?.summary =
resources?.getString(R.string.used_cache, chapterCache.readableSize)
}
.subscribe()
} }
class ClearDatabaseDialogController : DialogController() { class ClearDatabaseDialogController : DialogController() {
@@ -24,6 +24,9 @@ import eu.kanade.tachiyomi.ui.base.controller.BaseController
import eu.kanade.tachiyomi.ui.base.controller.RootController import eu.kanade.tachiyomi.ui.base.controller.RootController
import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.getResourceColor
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import rx.Observable
import rx.Subscription
import rx.subscriptions.CompositeSubscription
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@@ -32,14 +35,19 @@ abstract class SettingsController : PreferenceController() {
var preferenceKey: String? = null var preferenceKey: String? = null
val preferences: PreferencesHelper = Injekt.get() val preferences: PreferencesHelper = Injekt.get()
val viewScope = MainScope() val viewScope = MainScope()
private var themedContext: Context? = null
var untilDestroySubscriptions = CompositeSubscription()
private set
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle?): View {
if (untilDestroySubscriptions.isUnsubscribed) {
untilDestroySubscriptions = CompositeSubscription()
}
val view = super.onCreateView(inflater, container, savedInstanceState) val view = super.onCreateView(inflater, container, savedInstanceState)
if (this is RootController) { if (this is RootController) {
listView.clipToPadding = false view.updatePadding(bottom = view.context.resources.getDimensionPixelSize(R.dimen.action_toolbar_list_padding))
listView.updatePadding(bottom = view.context.resources.getDimensionPixelSize(R.dimen.action_toolbar_list_padding))
} }
listView.applyInsetter { listView.applyInsetter {
@@ -69,31 +77,25 @@ abstract class SettingsController : PreferenceController() {
} }
} }
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
if (type.isEnter) {
setTitle()
}
setHasOptionsMenu(type.isEnter)
super.onChangeStarted(handler, type)
}
override fun onDestroyView(view: View) { override fun onDestroyView(view: View) {
super.onDestroyView(view) super.onDestroyView(view)
themedContext = null untilDestroySubscriptions.unsubscribe()
} }
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
val tv = TypedValue() val screen = preferenceManager.createPreferenceScreen(getThemedContext())
activity!!.theme.resolveAttribute(R.attr.preferenceTheme, tv, true)
themedContext = ContextThemeWrapper(activity, tv.resourceId)
val screen = preferenceManager.createPreferenceScreen(themedContext)
preferenceScreen = screen preferenceScreen = screen
setupPreferenceScreen(screen) setupPreferenceScreen(screen)
} }
abstract fun setupPreferenceScreen(screen: PreferenceScreen): PreferenceScreen abstract fun setupPreferenceScreen(screen: PreferenceScreen): PreferenceScreen
private fun getThemedContext(): Context {
val tv = TypedValue()
activity!!.theme.resolveAttribute(R.attr.preferenceTheme, tv, true)
return ContextThemeWrapper(activity, tv.resourceId)
}
private fun animatePreferenceHighlight(view: View) { private fun animatePreferenceHighlight(view: View) {
ValueAnimator ValueAnimator
.ofObject(ArgbEvaluator(), Color.TRANSPARENT, view.context.getResourceColor(R.attr.rippleColor)) .ofObject(ArgbEvaluator(), Color.TRANSPARENT, view.context.getResourceColor(R.attr.rippleColor))
@@ -109,7 +111,7 @@ abstract class SettingsController : PreferenceController() {
return preferenceScreen?.title?.toString() return preferenceScreen?.title?.toString()
} }
private fun setTitle() { fun setTitle() {
var parentController = parentController var parentController = parentController
while (parentController != null) { while (parentController != null) {
if (parentController is BaseController<*> && parentController.getTitle() != null) { if (parentController is BaseController<*> && parentController.getTitle() != null) {
@@ -120,4 +122,16 @@ abstract class SettingsController : PreferenceController() {
(activity as? AppCompatActivity)?.supportActionBar?.title = getTitle() (activity as? AppCompatActivity)?.supportActionBar?.title = getTitle()
} }
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
if (type.isEnter) {
setTitle()
}
setHasOptionsMenu(type.isEnter)
super.onChangeStarted(handler, type)
}
fun <T> Observable<T>.subscribeUntilDestroy(onNext: (T) -> Unit): Subscription {
return subscribe(onNext).also { untilDestroySubscriptions.add(it) }
}
} }
@@ -132,7 +132,6 @@ class SettingsGeneralController : SettingsController() {
entriesRes = arrayOf( entriesRes = arrayOf(
R.string.theme_dark_default, R.string.theme_dark_default,
R.string.theme_dark_blue, R.string.theme_dark_blue,
R.string.theme_dark_amoledblue,
R.string.theme_dark_amoled, R.string.theme_dark_amoled,
R.string.theme_dark_red, R.string.theme_dark_red,
R.string.theme_dark_midnightdusk, R.string.theme_dark_midnightdusk,
@@ -141,7 +140,6 @@ class SettingsGeneralController : SettingsController() {
entryValues = arrayOf( entryValues = arrayOf(
Values.DarkThemeVariant.default.name, Values.DarkThemeVariant.default.name,
Values.DarkThemeVariant.blue.name, Values.DarkThemeVariant.blue.name,
Values.DarkThemeVariant.amoledblue.name,
Values.DarkThemeVariant.amoled.name, Values.DarkThemeVariant.amoled.name,
Values.DarkThemeVariant.red.name, Values.DarkThemeVariant.red.name,
Values.DarkThemeVariant.midnightdusk.name, Values.DarkThemeVariant.midnightdusk.name,
@@ -93,12 +93,11 @@ class SettingsReaderController : SettingsController() {
titleRes = R.string.pref_rotation_type titleRes = R.string.pref_rotation_type
entriesRes = arrayOf( entriesRes = arrayOf(
R.string.rotation_free, R.string.rotation_free,
R.string.rotation_portrait, R.string.rotation_lock,
R.string.rotation_landscape,
R.string.rotation_force_portrait, R.string.rotation_force_portrait,
R.string.rotation_force_landscape, R.string.rotation_force_landscape
) )
entryValues = arrayOf("1", "2", "3", "4", "5") entryValues = arrayOf("1", "2", "3", "4")
defaultValue = "1" defaultValue = "1"
summary = "%s" summary = "%s"
} }
@@ -6,6 +6,7 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@@ -32,7 +33,17 @@ class SettingsSearchController :
setHasOptionsMenu(true) setHasOptionsMenu(true)
} }
override fun createBinding(inflater: LayoutInflater) = SettingsSearchControllerBinding.inflate(inflater) /**
* Initiate the view with [R.layout.settings_search_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 {
binding = SettingsSearchControllerBinding.inflate(inflater)
return binding.root
}
override fun getTitle(): String? { override fun getTitle(): String? {
return presenter.query return presenter.query
@@ -139,10 +139,8 @@ class WebViewActivity : BaseViewBindingActivity<WebviewActivityBinding>() {
} }
override fun onDestroy() { override fun onDestroy() {
binding.webview?.destroy()
super.onDestroy() super.onDestroy()
// Binding sometimes isn't actually instantiated yet somehow
binding?.webview?.destroy()
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
@@ -2,19 +2,15 @@ package eu.kanade.tachiyomi.util
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Build
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.storage.getUriCompat import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.createFileInCacheDir import eu.kanade.tachiyomi.util.system.createFileInCacheDir
import eu.kanade.tachiyomi.util.system.notificationBuilder import eu.kanade.tachiyomi.util.system.notificationBuilder
import eu.kanade.tachiyomi.util.system.notificationManager import eu.kanade.tachiyomi.util.system.notificationManager
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import exh.syDebugVersion import java.io.IOException
class CrashLogUtil(private val context: Context) { class CrashLogUtil(private val context: Context) {
@@ -23,45 +19,31 @@ class CrashLogUtil(private val context: Context) {
} }
fun dumpLogs() { fun dumpLogs() {
launchIO { try {
try { val file = context.createFileInCacheDir("tachiyomi_crash_logs.txt")
val file = context.createFileInCacheDir("tachiyomi_crash_logs.txt") Runtime.getRuntime().exec("logcat *:E -d -f ${file.absolutePath}")
Runtime.getRuntime().exec("logcat *:E -d -f ${file.absolutePath}").waitFor()
file.appendText(getDebugInfo())
showNotification(file.getUriCompat(context)) showNotification(file.getUriCompat(context))
} catch (e: Throwable) { } catch (e: IOException) {
withUIContext { context.toast("Failed to get logs") } context.toast("Failed to get logs")
}
} }
} }
fun getDebugInfo(): String {
return """
App version: ${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}, ${BuildConfig.COMMIT_SHA}, ${BuildConfig.VERSION_CODE})
Preview build: $syDebugVersion
Android version: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT})
Android build ID: ${Build.DISPLAY}
Device brand: ${Build.BRAND}
Device manufacturer: ${Build.MANUFACTURER}
Device name: ${Build.DEVICE}
Device model: ${Build.MODEL}
Device product name: ${Build.PRODUCT}
""".trimIndent()
}
private fun showNotification(uri: Uri) { private fun showNotification(uri: Uri) {
context.notificationManager.cancel(Notifications.ID_CRASH_LOGS) context.notificationManager.cancel(Notifications.ID_CRASH_LOGS)
with(notificationBuilder) { with(notificationBuilder) {
setContentTitle(context.getString(R.string.crash_log_saved)) setContentTitle(context.getString(R.string.crash_log_saved))
// Clear old actions if they exist
clearActions() clearActions()
addAction( addAction(
R.drawable.ic_folder_24dp, R.drawable.ic_folder_24dp,
context.getString(R.string.action_open_log), context.getString(R.string.action_open_log),
NotificationReceiver.openErrorLogPendingActivity(context, uri) NotificationReceiver.openErrorLogPendingActivity(context, uri)
) )
addAction( addAction(
R.drawable.ic_share_24dp, R.drawable.ic_share_24dp,
context.getString(R.string.action_share), context.getString(R.string.action_share),
@@ -173,5 +173,3 @@ private fun shouldUpdateDbChapter(dbChapter: Chapter, sourceChapter: SChapter):
dbChapter.date_upload != sourceChapter.date_upload || dbChapter.date_upload != sourceChapter.date_upload ||
dbChapter.chapter_number != sourceChapter.chapter_number dbChapter.chapter_number != sourceChapter.chapter_number
} }
class NoChaptersException : Exception()
@@ -0,0 +1,3 @@
package eu.kanade.tachiyomi.util.chapter
class NoChaptersException : Exception()
@@ -0,0 +1,7 @@
package eu.kanade.tachiyomi.util.lang
inline fun <reified T : Enum<T>> T.next(): T {
val values = enumValues<T>()
val nextOrdinal = (ordinal + 1) % values.size
return values[nextOrdinal]
}
@@ -1,17 +1,12 @@
package eu.kanade.tachiyomi.util.system package eu.kanade.tachiyomi.util.system
import android.content.Context import android.content.Context
import android.os.Build
import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators import androidx.biometric.BiometricManager.Authenticators
object BiometricUtil { object BiometricUtil {
fun getSupportedAuthenticators(context: Context): Int { fun getSupportedAuthenticators(context: Context): Int {
if (isLegacySecured(context)) {
return Authenticators.BIOMETRIC_WEAK or Authenticators.DEVICE_CREDENTIAL
}
return listOf( return listOf(
Authenticators.BIOMETRIC_STRONG, Authenticators.BIOMETRIC_STRONG,
Authenticators.BIOMETRIC_WEAK, Authenticators.BIOMETRIC_WEAK,
@@ -22,22 +17,10 @@ object BiometricUtil {
} }
fun isSupported(context: Context): Boolean { fun isSupported(context: Context): Boolean {
return isLegacySecured(context) || getSupportedAuthenticators(context) != 0 return getSupportedAuthenticators(context) != 0
} }
fun isDeviceCredentialAllowed(context: Context): Boolean { fun isDeviceCredentialAllowed(context: Context): Boolean {
return isLegacySecured(context) || (getSupportedAuthenticators(context) and Authenticators.DEVICE_CREDENTIAL != 0) return getSupportedAuthenticators(context) and Authenticators.DEVICE_CREDENTIAL != 0
}
/**
* Returns whether the device is secured with a PIN, pattern or password.
*/
private fun isLegacySecured(context: Context): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
if (context.keyguardManager.isDeviceSecure) {
return true
}
}
return false
} }
} }
@@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.util.system package eu.kanade.tachiyomi.util.system
import android.app.ActivityManager import android.app.ActivityManager
import android.app.KeyguardManager
import android.app.Notification import android.app.Notification
import android.app.NotificationManager import android.app.NotificationManager
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
@@ -34,7 +33,6 @@ import androidx.core.net.toUri
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.lang.truncateCenter import eu.kanade.tachiyomi.util.lang.truncateCenter
import timber.log.Timber
import java.io.File import java.io.File
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -70,15 +68,10 @@ fun Context.toast(text: String?, duration: Int = Toast.LENGTH_SHORT, block: (Toa
fun Context.copyToClipboard(label: String, content: String) { fun Context.copyToClipboard(label: String, content: String) {
if (content.isBlank()) return if (content.isBlank()) return
try { val clipboard = getSystemService<ClipboardManager>()!!
val clipboard = getSystemService<ClipboardManager>()!! clipboard.setPrimaryClip(ClipData.newPlainText(label, content))
clipboard.setPrimaryClip(ClipData.newPlainText(label, content))
toast(getString(R.string.copied_to_clipboard, content.truncateCenter(50))) toast(getString(R.string.copied_to_clipboard, content.truncateCenter(50)))
} catch (e: Throwable) {
Timber.e(e)
toast(R.string.clipboard_copy_error)
}
} }
/** /**
@@ -160,18 +153,24 @@ val Float.dpToPxEnd: Float
val Resources.isLTR val Resources.isLTR
get() = configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR get() = configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR
/**
* Property to get the notification manager from the context.
*/
val Context.notificationManager: NotificationManager val Context.notificationManager: NotificationManager
get() = getSystemService()!! get() = getSystemService()!!
/**
* Property to get the connectivity manager from the context.
*/
val Context.connectivityManager: ConnectivityManager val Context.connectivityManager: ConnectivityManager
get() = getSystemService()!! get() = getSystemService()!!
/**
* Property to get the power manager from the context.
*/
val Context.powerManager: PowerManager val Context.powerManager: PowerManager
get() = getSystemService()!! get() = getSystemService()!!
val Context.keyguardManager: KeyguardManager
get() = getSystemService()!!
/** /**
* Convenience method to acquire a partial wake lock. * Convenience method to acquire a partial wake lock.
*/ */
@@ -84,20 +84,15 @@ object ImageUtil {
} }
/** /**
* Check whether the image is a double-page spread * Check whether the image is a double image (width > height), return the result and original stream
* @return true if the width is greater than the height
*/ */
fun isDoublePage(imageStream: InputStream): Boolean { fun isDoublePage(imageStream: InputStream): Pair<Boolean, InputStream> {
imageStream.mark(imageStream.available() + 1)
val imageBytes = imageStream.readBytes() val imageBytes = imageStream.readBytes()
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size, options) BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size, options)
imageStream.reset() return Pair(options.outWidth > options.outHeight, ByteArrayInputStream(imageBytes))
return options.outWidth > options.outHeight
} }
/** /**
@@ -1,26 +0,0 @@
package eu.kanade.tachiyomi.util.system
import android.content.Context
import android.content.res.Resources
object InternalResourceHelper {
fun getBoolean(context: Context, resName: String, defaultValue: Boolean): Boolean {
val id = getResourceId(resName, "bool")
return if (id != 0) {
context.createPackageContext("android", 0).resources.getBoolean(id)
} else {
defaultValue
}
}
/**
* Get resource id from system resources
* @param resName resource name to get
* @param type resource type of [resName] to get
* @return 0 if not available
*/
private fun getResourceId(resName: String, type: String): Int {
return Resources.getSystem().getIdentifier(resName, type, "android")
}
}
@@ -1,21 +1,19 @@
package eu.kanade.tachiyomi.util.view package eu.kanade.tachiyomi.util.view
import android.widget.ImageView import android.widget.ImageView
import androidx.annotation.AttrRes
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import eu.kanade.tachiyomi.util.system.getResourceColor
/** /**
* Set a vector on a [ImageView]. * Set a vector on a [ImageView].
* *
* @param drawable id of drawable resource * @param drawable id of drawable resource
*/ */
fun ImageView.setVectorCompat(@DrawableRes drawable: Int, @AttrRes tint: Int? = null) { fun ImageView.setVectorCompat(@DrawableRes drawable: Int, tint: Int? = null) {
val vector = AppCompatResources.getDrawable(context, drawable) val vector = AppCompatResources.getDrawable(context, drawable)
if (tint != null) { if (tint != null) {
vector?.mutate() vector?.mutate()
vector?.setTint(context.getResourceColor(tint)) vector?.setTint(tint)
} }
setImageDrawable(vector) setImageDrawable(vector)
} }
@@ -2,7 +2,6 @@
package eu.kanade.tachiyomi.util.view package eu.kanade.tachiyomi.util.view
import android.annotation.SuppressLint
import android.graphics.Point import android.graphics.Point
import android.view.Gravity import android.view.Gravity
import android.view.Menu import android.view.Menu
@@ -10,18 +9,14 @@ import android.view.MenuItem
import android.view.View import android.view.View
import androidx.annotation.MenuRes import androidx.annotation.MenuRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.view.menu.MenuBuilder
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.appcompat.widget.TooltipCompat import androidx.appcompat.widget.TooltipCompat
import androidx.core.content.ContextCompat
import androidx.core.view.forEach
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup import com.google.android.material.chip.ChipGroup
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.getResourceColor
/** /**
* Returns coordinates of view. * Returns coordinates of view.
@@ -68,7 +63,7 @@ inline fun View.setTooltip(@StringRes stringRes: Int) {
inline fun View.popupMenu( inline fun View.popupMenu(
@MenuRes menuRes: Int, @MenuRes menuRes: Int,
noinline initMenu: (Menu.() -> Unit)? = null, noinline initMenu: (Menu.() -> Unit)? = null,
noinline onMenuItemClick: MenuItem.() -> Unit noinline onMenuItemClick: MenuItem.() -> Boolean
): PopupMenu { ): PopupMenu {
val popup = PopupMenu(context, this, Gravity.NO_GRAVITY, R.attr.actionOverflowMenuStyle, 0) val popup = PopupMenu(context, this, Gravity.NO_GRAVITY, R.attr.actionOverflowMenuStyle, 0)
popup.menuInflater.inflate(menuRes, popup.menu) popup.menuInflater.inflate(menuRes, popup.menu)
@@ -76,50 +71,7 @@ inline fun View.popupMenu(
if (initMenu != null) { if (initMenu != null) {
popup.menu.initMenu() popup.menu.initMenu()
} }
popup.setOnMenuItemClickListener { popup.setOnMenuItemClickListener { it.onMenuItemClick() }
it.onMenuItemClick()
true
}
popup.show()
return popup
}
/**
* Shows a popup menu on top of this view.
*
* @param items menu item names to inflate the menu with. List of itemId to stringRes pairs.
* @param selectedItemId optionally show a checkmark beside an item with this itemId.
* @param onMenuItemClick function to execute when a menu item is clicked.
*/
@SuppressLint("RestrictedApi")
inline fun View.popupMenu(
items: List<Pair<Int, Int>>,
selectedItemId: Int? = null,
noinline onMenuItemClick: MenuItem.() -> Unit
): PopupMenu {
val popup = PopupMenu(context, this, Gravity.NO_GRAVITY, R.attr.actionOverflowMenuStyle, 0)
items.forEach { (id, stringRes) ->
popup.menu.add(0, id, 0, stringRes)
}
if (selectedItemId != null) {
(popup.menu as? MenuBuilder)?.setOptionalIconsVisible(true)
val emptyIcon = ContextCompat.getDrawable(context, R.drawable.ic_blank_24dp)
popup.menu.forEach { item ->
item.icon = when (item.itemId) {
selectedItemId -> ContextCompat.getDrawable(context, R.drawable.ic_check_24dp)?.mutate()?.apply {
setTint(context.getResourceColor(android.R.attr.textColorPrimary))
}
else -> emptyIcon
}
}
}
popup.setOnMenuItemClickListener {
it.onMenuItemClick()
true
}
popup.show() popup.show()
return popup return popup
@@ -12,7 +12,7 @@ import androidx.annotation.MenuRes
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.core.view.isVisible import androidx.core.view.isVisible
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.ActionToolbarBinding import eu.kanade.tachiyomi.databinding.CommonActionToolbarBinding
/** /**
* A toolbar holding only menu items. * A toolbar holding only menu items.
@@ -20,21 +20,25 @@ import eu.kanade.tachiyomi.databinding.ActionToolbarBinding
class ActionToolbar @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : class ActionToolbar @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
FrameLayout(context, attrs) { FrameLayout(context, attrs) {
private val binding = ActionToolbarBinding.inflate(LayoutInflater.from(context), this, true) private val binding: CommonActionToolbarBinding
init {
binding = CommonActionToolbarBinding.inflate(LayoutInflater.from(context), this, true)
}
/** /**
* Remove menu items and remove listener. * Remove menu items and remove listener.
*/ */
fun destroy() { fun destroy() {
binding.menu.menu.clear() binding.commonActionMenu.menu.clear()
binding.menu.setOnMenuItemClickListener(null) binding.commonActionMenu.setOnMenuItemClickListener(null)
} }
/** /**
* Gets a menu item if found. * Gets a menu item if found.
*/ */
fun findItem(@IdRes itemId: Int): MenuItem? { fun findItem(@IdRes itemId: Int): MenuItem? {
return binding.menu.menu.findItem(itemId) return binding.commonActionMenu.menu.findItem(itemId)
} }
/** /**
@@ -42,14 +46,14 @@ class ActionToolbar @JvmOverloads constructor(context: Context, attrs: Attribute
*/ */
fun show(mode: ActionMode, @MenuRes menuRes: Int, listener: (item: MenuItem?) -> Boolean) { fun show(mode: ActionMode, @MenuRes menuRes: Int, listener: (item: MenuItem?) -> Boolean) {
// Avoid re-inflating the menu // Avoid re-inflating the menu
if (binding.menu.menu.size() == 0) { if (binding.commonActionMenu.menu.size() == 0) {
mode.menuInflater.inflate(menuRes, binding.menu.menu) mode.menuInflater.inflate(menuRes, binding.commonActionMenu.menu)
binding.menu.setOnMenuItemClickListener { listener(it) } binding.commonActionMenu.setOnMenuItemClickListener { listener(it) }
} }
binding.actionToolbar.isVisible = true binding.commonActionToolbar.isVisible = true
val bottomAnimation = AnimationUtils.loadAnimation(context, R.anim.enter_from_bottom) val bottomAnimation = AnimationUtils.loadAnimation(context, R.anim.enter_from_bottom)
binding.actionToolbar.startAnimation(bottomAnimation) binding.commonActionToolbar.startAnimation(bottomAnimation)
} }
/** /**
@@ -60,10 +64,10 @@ class ActionToolbar @JvmOverloads constructor(context: Context, attrs: Attribute
bottomAnimation.setAnimationListener( bottomAnimation.setAnimationListener(
object : SimpleAnimationListener() { object : SimpleAnimationListener() {
override fun onAnimationEnd(animation: Animation) { override fun onAnimationEnd(animation: Animation) {
binding.actionToolbar.isVisible = false binding.commonActionToolbar.isVisible = false
} }
} }
) )
binding.actionToolbar.startAnimation(bottomAnimation) binding.commonActionToolbar.startAnimation(bottomAnimation)
} }
} }
@@ -1,10 +1,13 @@
package eu.kanade.tachiyomi.widget.materialdialogs package eu.kanade.tachiyomi.widget.materialdialogs
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet import android.util.AttributeSet
import androidx.annotation.AttrRes
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.widget.AppCompatImageView import androidx.appcompat.widget.AppCompatImageView
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.view.setVectorCompat import eu.kanade.tachiyomi.util.system.getResourceColor
class QuadStateCheckBox @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : class QuadStateCheckBox @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
AppCompatImageView(context, attrs) { AppCompatImageView(context, attrs) {
@@ -16,11 +19,19 @@ class QuadStateCheckBox @JvmOverloads constructor(context: Context, attrs: Attri
} }
private fun updateDrawable() { private fun updateDrawable() {
when (state) { val drawable = when (state) {
State.UNCHECKED -> setVectorCompat(R.drawable.ic_check_box_outline_blank_24dp, R.attr.colorControlNormal) State.UNCHECKED -> tintVector(context, R.drawable.ic_check_box_outline_blank_24dp, R.attr.colorControlNormal)
State.INDETERMINATE -> setVectorCompat(R.drawable.ic_indeterminate_check_box_24dp, R.attr.colorAccent) State.INDETERMINATE -> tintVector(context, R.drawable.ic_indeterminate_check_box_24dp)
State.CHECKED -> setVectorCompat(R.drawable.ic_check_box_24dp, R.attr.colorAccent) State.CHECKED -> tintVector(context, R.drawable.ic_check_box_24dp)
State.INVERSED -> setVectorCompat(R.drawable.ic_check_box_x_24dp, R.attr.colorAccent) State.INVERSED -> tintVector(context, R.drawable.ic_check_box_x_24dp)
}
setImageDrawable(drawable)
}
private fun tintVector(context: Context, resId: Int, @AttrRes colorAttrRes: Int = R.attr.colorAccent): Drawable {
return AppCompatResources.getDrawable(context, resId)!!.apply {
setTint(context.getResourceColor(colorAttrRes))
} }
} }

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