Compare commits

...

43 Commits

Author SHA1 Message Date
Jobobby04 2cf0475066 Hide dedupe by priority 2020-12-21 14:46:38 -05:00
Jobobby04 0923cd6509 Release 1.4.1 2020-12-21 14:41:49 -05:00
Jobobby04 330908c49d Add continues vertical crop borders to the reading settings 2020-12-21 14:39:54 -05:00
Jobobby04 644140b617 Make a coroutine presenter 2020-12-21 14:21:43 -05:00
Jobobby04 d302a0fbc7 Cleanup 2020-12-21 14:21:43 -05:00
Jobobby04 ce8f7da9ca Use ContextCompat to get custom source icons 2020-12-21 14:21:42 -05:00
Jobobby04 f1a4811030 In batch add and deeplink, only use enabled sources 2020-12-21 14:21:42 -05:00
Jobobby04 a439ffcafc Add separate continues vertical crop borders setting 2020-12-21 14:21:42 -05:00
Jobobby04 5eeab103c2 Fix external repo preference conflict 2020-12-21 14:21:41 -05:00
Jobobby04 78ffd9d56e Cleanup 2020-12-21 14:21:41 -05:00
Jobobby04 96213900ac Add external repo support 2020-12-21 14:21:40 -05:00
Jobobby04 85e30ef6ca Cleanup 2020-12-21 14:21:40 -05:00
arkon f38df69983 Update app repo URL
(cherry picked from commit 04fff91e23)

# Conflicts:
#	.github/workflows/TachiyomiSY-Release-Builder.yml
#	README.md
#	app/src/main/java/eu/kanade/tachiyomi/data/updater/github/GithubUpdateChecker.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/main/WhatsNewDialogController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutController.kt
2020-12-21 14:21:40 -05:00
arkon 64e9515293 Update extensions repo URL
(cherry picked from commit 28a23452f2)

# Conflicts:
#	README.md
2020-12-21 14:21:39 -05:00
arkon 67e676d4ae Apply theme to OAuth login redirect activities
(cherry picked from commit 6d403851cf)
2020-12-21 14:21:39 -05:00
arkon ef36a9c28c Misc tracker code cleanup
(cherry picked from commit 395a749bce)
2020-12-21 14:21:38 -05:00
arkon 513bcbb80d Refactor CustomTabsIntent creation
(cherry picked from commit 2cc2a90941)
2020-12-21 14:21:38 -05:00
Jobobby04 3d5952ebbd Cleanup some unneeded lambas 2020-12-21 14:21:38 -05:00
arkon 1d55a1bec4 Fix loading fallback thumbnails in browse view (closes #4127)
(cherry picked from commit c5ca739b49)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt
2020-12-21 14:21:37 -05:00
arkon 962344f5fc Minor code cleanup
(cherry picked from commit 00fe4cdf2d)
2020-12-21 14:21:37 -05:00
Jobobby04 ba6bcc82b6 More dialog fixes 2020-12-21 14:21:36 -05:00
arkon 6659935f3d Complete migration off of Kotlin synthetics
(cherry picked from commit 69be3e1e87)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt
2020-12-21 14:21:36 -05:00
arkon ccca9e8828 Show MAL relogin message on update, localize error message
(cherry picked from commit 2cb3984d68)
2020-12-21 14:21:36 -05:00
arkon b4cce2b3e0 Use view binding for date headers
(cherry picked from commit 5901978889)
2020-12-21 14:21:35 -05:00
arkon 9737d847fd Update to coroutines 1.4.2
Should fix crashes on some devices. See https://github.com/Kotlin/kotlinx.coroutines/issues/2371

(cherry picked from commit 8bf1cf3cc5)
2020-12-21 14:21:35 -05:00
arkon f180c6a07c Reword NSFW settings section
(cherry picked from commit f6af1184bc)
2020-12-21 14:21:35 -05:00
arkon 024c2d4ce0 More crash fixes
(cherry picked from commit 4880741b8b)
2020-12-21 14:21:34 -05:00
arkon 17731f3904 Remove bundled fallback file picker
(cherry picked from commit e8627800fe)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt
#	app/src/main/res/layout/common_listitem_dir.xml
#	app/src/main/res/values/styles.xml
2020-12-21 14:21:34 -05:00
arkon e2dadd4213 Require WebView 86+
(cherry picked from commit 907fbb94a2)
2020-12-21 14:21:34 -05:00
Jobobby04 b4fedf9a87 Maybe fix push to mangadex 2020-12-21 14:21:33 -05:00
Jobobby04 beaf6284fd Fixes to merged settings dialog and edit manga dialog 2020-12-21 14:21:33 -05:00
arkon 3300eb0e79 Some crash fixes
(cherry picked from commit fd2028557e)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt
2020-12-21 14:21:33 -05:00
arkon 3599526fde Suppress some deprecation warnings
(cherry picked from commit 91fa1ec6b2)
2020-12-21 14:21:32 -05:00
arkon 8b6a0ad891 Note that toggling NSFW sources requires a restart
(cherry picked from commit bbc00768f0)
2020-12-21 14:21:32 -05:00
arkon cf99ee73f5 Break out NSFW hiding/labeling into separate settings
(cherry picked from commit 5b09461ccf)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceValues.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionHolder.kt
2020-12-21 14:21:31 -05:00
arkon bbd3e3c29c Remove source overwrite logic since built-in sources no longer exist
(cherry picked from commit 1a439ecece)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt
2020-12-21 14:21:31 -05:00
arkon 972579bbec Flip left/right key events for Webtoon viewer (fixes #4111)
(cherry picked from commit 836aec4396)
2020-12-21 14:21:31 -05:00
Jobobby04 4044b0897e Make S Pen action ids unique 2020-12-21 14:21:30 -05:00
Jobobby04 5e6c0bbc14 Change Search in Tachiyomi to Search in TachiyomiSY 2020-12-21 14:21:30 -05:00
she11sh0cked a8c6474f5e Use a different title for japanese manga vs other manga (#181)
* Use a different title for japanese manga vs other manga

* Revert wildcard import
2020-12-21 14:20:49 -05:00
jobobby04 820279634e [SKIP CI] Update readme 2020-12-12 14:47:55 -05:00
jobobby04 ce7577a2b4 [SKIP CI] Update readme 2020-12-12 14:46:35 -05:00
Jobobby04 31376e5a52 Revert "Hide incomplete NSFW source labelling settings"
This reverts commit 134f776a86.
2020-12-12 14:07:33 -05:00
142 changed files with 1354 additions and 963 deletions
+5 -5
View File
@@ -1,17 +1,17 @@
1. **Before reporting a new issue, take a look at the [FAQ](https://tachiyomi.org/help/faq/), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).** 1. **Before reporting a new issue, take a look at the [FAQ](https://tachiyomi.org/help/faq/), the [changelog](https://github.com/tachiyomiorg/tachiyomi/releases) and the already opened [issues](https://github.com/tachiyomiorg/tachiyomi/issues).**
2. If you are unsure, ask here: [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/tachiyomi) 2. If you are unsure, ask here: [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/tachiyomi)
3. What is your type of issue? 3. What is your type of issue?
* [Catalogue request](#catalogue-requests) * [Catalogue request](#catalogue-requests)
* [Bugs](#bugs) * [Bugs](#bugs)
* [Feature requests](#feature-requests) * [Feature requests](#feature-requests)
* [Translations](https://tachiyomi.org/help/contribution/#translation) * [Translations](https://tachiyomi.org/help/contribution/#translation)
4. After following 1. and 3. you can [open your issue](https://github.com/inorichi/tachiyomi/issues/new) 4. After following 1. and 3. you can [open your issue](https://github.com/tachiyomiorg/tachiyomi/issues/new)
*** ***
# Catalogue requests # Catalogue requests
* Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions#readme, not here * Catalogue requests should be created at https://github.com/tachiyomiorg/tachiyomi-extensions#readme, not here
# Bugs # Bugs
* Include version (More > About > Version) * Include version (More > About > Version)
@@ -23,9 +23,9 @@
* For large logs use http://pastebin.com/ (or similar) * For large logs use http://pastebin.com/ (or similar)
* Don't group unrelated requests into one issue * Don't group unrelated requests into one issue
DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71 DO: https://github.com/tachiyomiorg/tachiyomi/issues/24 https://github.com/tachiyomiorg/tachiyomi/issues/71
DON'T: https://github.com/inorichi/tachiyomi/issues/75 DON'T: https://github.com/tachiyomiorg/tachiyomi/issues/75
# Feature requests # Feature requests
+2 -2
View File
@@ -2,9 +2,9 @@
I acknowledge that: I acknowledge that:
- I have updated to the latest version of the app (stable is v1.4.0) - I have updated to the latest version of the app (stable is v1.4.1)
- I have updated all extensions - I have updated all extensions
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions - If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT** **DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
+2 -2
View File
@@ -9,9 +9,9 @@ labels: "bug"
I acknowledge that: I acknowledge that:
- I have updated to the latest version of the app (stable is v1.4.0) - I have updated to the latest version of the app (stable is v1.4.1)
- I have updated all extensions - I have updated all extensions
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions - If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT** **DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
+1 -1
View File
@@ -4,5 +4,5 @@ contact_links:
url: https://tachiyomi.org/help/ url: https://tachiyomi.org/help/
about: Common questions are answered here. about: Common questions are answered here.
- name: Tachiyomi extensions GitHub repository - name: Tachiyomi extensions GitHub repository
url: https://github.com/inorichi/tachiyomi-extensions url: https://github.com/tachiyomiorg/tachiyomi-extensions
about: Issues about an extension/source/catalogue should be opened here instead. about: Issues about an extension/source/catalogue should be opened here instead.
+2 -2
View File
@@ -9,9 +9,9 @@ labels: "feature"
I acknowledge that: I acknowledge that:
- I have updated to the latest version of the app (stable is v1.4.0) - I have updated to the latest version of the app (stable is v1.4.1)
- I have updated all extensions - I have updated all extensions
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions - If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT** **DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
+3 -3
View File
@@ -1,8 +1,8 @@
--- ---
name: "Extension/source/catalogue issue" name: "Extension/source/catalogue issue"
about: "Do not open an issue here. See https://github.com/inorichi/tachiyomi-extensions" about: "Do not open an issue here. See https://github.com/tachiyomiorg/tachiyomi-extensions"
title: "THIS ISSUE IS IN THE WRONG REPO; SEE https://github.com/inorichi/tachiyomi-extensions" title: "THIS ISSUE IS IN THE WRONG REPO; SEE https://github.com/tachiyomiorg/tachiyomi-extensions"
labels: "catalog, invalid" labels: "catalog, invalid"
--- ---
DO NOT OPEN AN ISSUE IN THIS REPO. SEE https://github.com/inorichi/tachiyomi-extensions DO NOT OPEN AN ISSUE IN THIS REPO. SEE https://github.com/tachiyomiorg/tachiyomi-extensions
@@ -18,7 +18,7 @@ jobs:
uses: gradle/wrapper-validation-action@v1 uses: gradle/wrapper-validation-action@v1
build: build:
name: Build app release name: Build app
needs: check_wrapper needs: check_wrapper
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')" if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -60,21 +60,16 @@ jobs:
dependencies-cache-enabled: true dependencies-cache-enabled: true
configuration-cache-enabled: true configuration-cache-enabled: true
- name: Sign Android Release - name: Sign APK
uses: r0adkll/sign-android-release@v1 uses: r0adkll/sign-android-release@v1
with: with:
# The directory to find your release to sign
releaseDirectory: app/build/outputs/apk/standard/release releaseDirectory: app/build/outputs/apk/standard/release
# The key used to sign your release in base64 encoded format
signingKeyBase64: ${{ secrets.SIGNING_KEY }} signingKeyBase64: ${{ secrets.SIGNING_KEY }}
# The key alias
alias: ${{ secrets.ALIAS }} alias: ${{ secrets.ALIAS }}
# The password to the keystore
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
# The password for the key
keyPassword: ${{ secrets.KEY_PASSWORD }} keyPassword: ${{ secrets.KEY_PASSWORD }}
- name: Create Release - name: Create release
id: create_release id: create_release
uses: actions/create-release@v1 uses: actions/create-release@v1
env: env:
@@ -85,7 +80,7 @@ jobs:
draft: true draft: true
prerelease: false prerelease: false
- name: Upload Release APK - name: Upload APK to release
uses: actions/upload-release-asset@v1 uses: actions/upload-release-asset@v1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+17 -16
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 sources such as MangaDex, MangaSee, Mangakakalot, [and more](https://github.com/inorichi/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
@@ -52,18 +52,19 @@ Features of TachiyomiSY include:
* Enhanced views for internal and integrated sources * Enhanced views for internal and integrated sources
* Enhanced usability for internal and delegated sources * Enhanced usability for internal and delegated sources
* Custom sources: Custom sources:
* * E-Hentai/ExHentai * E-Hentai/ExHentai
* Additional features for some extensions, features include custom description, opening in app, batch add to library, and a bunch of other things based on the source:
* * 8Muses (EroMuse) Additional features for some extensions, features include custom description, opening in app, batch add to library, and a bunch of other things based on the source:
* * HBrowse * 8Muses (EroMuse)
* * HentaiCafe (inside Foolside) * HBrowse
* * Hitomi.la * HentaiCafe (inside Foolside)
* * Mangadex * Hitomi.la
* * NHentai * Mangadex
* * PervEden (EN and IT) * NHentai
* * Puruin * PervEden (EN and IT)
* * Tsumino * Puruin
* Tsumino
## Download ## Download
Get the app from our [releases page](https://github.com/jobobby04/tachiyomisy/releases/latest). Get the app from our [releases page](https://github.com/jobobby04/tachiyomisy/releases/latest).
@@ -92,9 +93,9 @@ Please make sure to read the full guidelines. Your issue may be closed without w
* For large logs use http://pastebin.com/ (or similar) * For large logs use http://pastebin.com/ (or similar)
* Don't group unrelated requests into one issue * Don't group unrelated requests into one issue
DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71 DO: https://github.com/tachiyomiorg/tachiyomi/issues/24 https://github.com/tachiyomiorg/tachiyomi/issues/71
DON'T: https://github.com/inorichi/tachiyomi/issues/75 DON'T: https://github.com/tachiyomiorg/tachiyomi/issues/75
</details> </details>
@@ -103,7 +104,7 @@ DON'T: https://github.com/inorichi/tachiyomi/issues/75
* Write a detailed issue, explaining what it should do or how. Avoid writing just "like X app does" * Write a detailed issue, explaining what it should do or how. Avoid writing just "like X app does"
* Include screenshot (if needed) * Include screenshot (if needed)
Source requests should be created at https://github.com/inorichi/tachiyomi-extensions, they do not belong in this repository. Source requests should be created at https://github.com/tachiyomiorg/tachiyomi-extensions, they do not belong in this repository.
</details> </details>
## FAQ ## FAQ
+4 -9
View File
@@ -5,8 +5,8 @@ import java.text.SimpleDateFormat
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'com.mikepenz.aboutlibraries.plugin' apply plugin: 'com.mikepenz.aboutlibraries.plugin'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-parcelize'
apply plugin: 'kotlinx-serialization' apply plugin: 'kotlinx-serialization'
apply plugin: 'com.github.zellius.shortcut-helper' apply plugin: 'com.github.zellius.shortcut-helper'
// Realm (EH) // Realm (EH)
@@ -44,8 +44,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 10 versionCode 11
versionName "1.4.0" versionName "1.4.1"
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\"" buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\"" buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
@@ -133,10 +133,6 @@ android {
} }
} }
androidExtensions {
experimental = true
}
dependencies { dependencies {
// Source models and interfaces from Tachiyomi 1.x // Source models and interfaces from Tachiyomi 1.x
@@ -253,7 +249,6 @@ dependencies {
implementation 'com.github.dmytrodanylyk.android-process-button:library:1.0.4' implementation 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
implementation 'eu.davidea:flexible-adapter:5.1.0' implementation 'eu.davidea:flexible-adapter:5.1.0'
implementation 'eu.davidea:flexible-adapter-ui:1.0.0' implementation 'eu.davidea:flexible-adapter-ui:1.0.0'
implementation 'com.nononsenseapps:filepicker:2.5.2'
implementation 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0' implementation 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0'
implementation 'com.github.chrisbanes:PhotoView:2.3.0' implementation 'com.github.chrisbanes:PhotoView:2.3.0'
implementation 'com.github.carlosesco:DirectionalViewPager:a844dbca0a' implementation 'com.github.carlosesco:DirectionalViewPager:a844dbca0a'
@@ -294,7 +289,7 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-reflect:$BuildPluginsVersion.KOTLIN" implementation "org.jetbrains.kotlin:kotlin-reflect:$BuildPluginsVersion.KOTLIN"
final coroutines_version = '1.4.1' final coroutines_version = '1.4.2'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
-4
View File
@@ -80,10 +80,6 @@
<activity <activity
android:name=".ui.webview.WebViewActivity" android:name=".ui.webview.WebViewActivity"
android:configChanges="uiMode|orientation|screenSize" /> android:configChanges="uiMode|orientation|screenSize" />
<activity
android:name=".widget.CustomLayoutPickerActivity"
android:label="@string/app_name"
android:theme="@style/FilePickerTheme" />
<activity <activity
android:name=".ui.setting.track.AnilistLoginActivity" android:name=".ui.setting.track.AnilistLoginActivity"
android:label="Anilist"> android:label="Anilist">
@@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.updater.UpdaterJob import eu.kanade.tachiyomi.data.updater.UpdaterJob
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
import eu.kanade.tachiyomi.ui.library.LibrarySort import eu.kanade.tachiyomi.ui.library.LibrarySort
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.widget.ExtendedNavigationView import eu.kanade.tachiyomi.widget.ExtendedNavigationView
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@@ -120,7 +121,10 @@ object Migrations {
// Force MAL log out due to login flow change // Force MAL log out due to login flow change
val trackManager = Injekt.get<TrackManager>() val trackManager = Injekt.get<TrackManager>()
trackManager.myAnimeList.logout() if (trackManager.myAnimeList.isLogged) {
trackManager.myAnimeList.logout()
context.toast(R.string.myanimelist_relogin)
}
} }
return true return true
} }
@@ -146,7 +146,7 @@ class BackupNotifier(private val context: Context) {
val uri = destFile.getUriCompat(context) val uri = destFile.getUriCompat(context)
addAction( addAction(
R.drawable.nnf_ic_file_folder, 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)
) )
@@ -137,7 +137,7 @@ class DownloadPendingDeleter(context: Context) {
val id: Long, val id: Long,
val url: String, val url: String,
val name: String, val name: String,
val scanlator: String? val scanlator: String? = null
) )
/** /**
@@ -110,7 +110,7 @@ class LibraryUpdateNotifier(private val context: Context) {
setContentIntent(errorLogIntent) setContentIntent(errorLogIntent)
addAction( addAction(
R.drawable.nnf_ic_file_folder, R.drawable.ic_folder_24dp,
context.getString(R.string.action_open_log), context.getString(R.string.action_open_log),
errorLogIntent errorLogIntent
) )
@@ -39,6 +39,7 @@ import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.isServiceRunning import eu.kanade.tachiyomi.util.system.isServiceRunning
import exh.LIBRARY_UPDATE_EXCLUDED_SOURCES import exh.LIBRARY_UPDATE_EXCLUDED_SOURCES
import exh.MERGED_SOURCE_ID import exh.MERGED_SOURCE_ID
import exh.mangaDexSourceIds
import exh.md.utils.FollowStatus import exh.md.utils.FollowStatus
import exh.md.utils.MdUtil import exh.md.utils.MdUtil
import exh.metadata.metadata.base.insertFlatMetadata import exh.metadata.metadata.base.insertFlatMetadata
@@ -283,7 +284,7 @@ class LibraryUpdateService(
val trackingExtra = intent.getStringExtra(KEY_GROUP_EXTRA)?.toIntOrNull() ?: -1 val trackingExtra = intent.getStringExtra(KEY_GROUP_EXTRA)?.toIntOrNull() ?: -1
libraryManga.filter { libraryManga.filter {
val loggedServices = trackManager.services.filter { it.isLogged } val loggedServices = trackManager.services.filter { it.isLogged }
val status: String = { val status: String = run {
val tracks = db.getTracks(it).executeAsBlocking() val tracks = db.getTracks(it).executeAsBlocking()
val track = tracks.find { track -> val track = tracks.find { track ->
loggedServices.any { it.id == track?.sync_id } loggedServices.any { it.id == track?.sync_id }
@@ -294,7 +295,7 @@ class LibraryUpdateService(
} else { } else {
"not tracked" "not tracked"
} }
}() }
trackManager.mapTrackingOrder(status, applicationContext) == trackingExtra trackManager.mapTrackingOrder(status, applicationContext) == trackingExtra
} }
} }
@@ -537,7 +538,9 @@ class LibraryUpdateService(
} }
// SY --> // SY -->
// filter all follows from Mangadex and only add reading or rereading manga to library /**
* filter all follows from Mangadex and only add reading or rereading manga to library
*/
private fun syncFollows(): Observable<LibraryManga> { private fun syncFollows(): Observable<LibraryManga> {
val count = AtomicInteger(0) val count = AtomicInteger(0)
val mangaDex = MdUtil.getEnabledMangaDex(preferences, sourceManager) ?: return Observable.empty() val mangaDex = MdUtil.getEnabledMangaDex(preferences, sourceManager) ?: return Observable.empty()
@@ -582,7 +585,7 @@ class LibraryUpdateService(
*/ */
private fun pushFavorites(): Observable<LibraryManga> { private fun pushFavorites(): Observable<LibraryManga> {
val count = AtomicInteger(0) val count = AtomicInteger(0)
val listManga = db.getLibraryMangas().executeAsBlocking() val listManga = db.getFavoriteMangas().executeAsBlocking().filter { it.source in mangaDexSourceIds }
// filter all follows from Mangadex and only add reading or rereading manga to library // filter all follows from Mangadex and only add reading or rereading manga to library
return Observable.from(if (trackManager.mdList.isLogged) listManga else emptyList()) return Observable.from(if (trackManager.mdList.isLogged) listManga else emptyList())
@@ -127,7 +127,9 @@ object PreferenceKeys {
const val automaticExtUpdates = "automatic_ext_updates" const val automaticExtUpdates = "automatic_ext_updates"
const val allowNsfwSource = "allow_nsfw_source" const val showNsfwSource = "show_nsfw_source"
const val showNsfwExtension = "show_nsfw_extension"
const val labelNsfwExtension = "label_nsfw_extension"
const val startScreen = "start_screen" const val startScreen = "start_screen"
@@ -330,4 +332,8 @@ object PreferenceKeys {
const val createLegacyBackup = "create_legacy_backup" const val createLegacyBackup = "create_legacy_backup"
const val dontDeleteFromCategories = "dont_delete_from_categories" const val dontDeleteFromCategories = "dont_delete_from_categories"
const val extensionRepos = "extension_repos"
const val cropBordersContinuesVertical = "crop_borders_continues_vertical"
} }
@@ -44,12 +44,6 @@ object PreferenceValues {
BOTH BOTH
} }
enum class NsfwAllowance {
ALLOWED,
PARTIAL,
BLOCKED
}
// SY --> // SY -->
enum class GroupLibraryMode { enum class GroupLibraryMode {
GLOBAL, GLOBAL,
@@ -10,7 +10,6 @@ import com.tfcporciuncula.flow.Preference
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
import eu.kanade.tachiyomi.data.preference.PreferenceValues.NsfwAllowance
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.anilist.Anilist import eu.kanade.tachiyomi.data.track.anilist.Anilist
import eu.kanade.tachiyomi.widget.ExtendedNavigationView import eu.kanade.tachiyomi.widget.ExtendedNavigationView
@@ -231,7 +230,9 @@ class PreferencesHelper(val context: Context) {
fun automaticExtUpdates() = flowPrefs.getBoolean(Keys.automaticExtUpdates, true) fun automaticExtUpdates() = flowPrefs.getBoolean(Keys.automaticExtUpdates, true)
fun allowNsfwSource() = flowPrefs.getEnum(Keys.allowNsfwSource, NsfwAllowance.ALLOWED) fun showNsfwSource() = flowPrefs.getBoolean(Keys.showNsfwSource, true)
fun showNsfwExtension() = flowPrefs.getBoolean(Keys.showNsfwExtension, true)
fun labelNsfwExtension() = prefs.getBoolean(Keys.labelNsfwExtension, true)
fun extensionUpdatesCount() = flowPrefs.getInt("ext_updates_count", 0) fun extensionUpdatesCount() = flowPrefs.getInt("ext_updates_count", 0)
@@ -444,4 +445,8 @@ class PreferencesHelper(val context: Context) {
fun sortTagsForLibrary() = flowPrefs.getStringSet(Keys.sortTagsForLibrary, mutableSetOf()) fun sortTagsForLibrary() = flowPrefs.getStringSet(Keys.sortTagsForLibrary, mutableSetOf())
fun dontDeleteFromCategories() = flowPrefs.getStringSet(Keys.dontDeleteFromCategories, emptySet()) fun dontDeleteFromCategories() = flowPrefs.getStringSet(Keys.dontDeleteFromCategories, emptySet())
fun extensionRepos() = flowPrefs.getStringSet(Keys.extensionRepos, emptySet())
fun cropBordersContinuesVertical() = flowPrefs.getBoolean(Keys.cropBordersContinuesVertical, false)
} }
@@ -4,6 +4,7 @@ import android.net.Uri
import androidx.core.net.toUri import androidx.core.net.toUri
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@@ -18,9 +19,8 @@ import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long import kotlinx.serialization.json.long
import kotlinx.serialization.json.put import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonObject import kotlinx.serialization.json.putJsonObject
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import rx.Observable import rx.Observable
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@@ -30,7 +30,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
private val json: Json by injectLazy() private val json: Json by injectLazy()
private val jsonMime = "application/json; charset=utf-8".toMediaTypeOrNull() private val jsonMime = "application/json; charset=utf-8".toMediaType()
private val authClient = client.newBuilder().addInterceptor(interceptor).build() private val authClient = client.newBuilder().addInterceptor(interceptor).build()
fun addLibManga(track: Track): Observable<Track> { fun addLibManga(track: Track): Observable<Track> {
@@ -51,22 +51,18 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("status", track.toAnilistStatus()) put("status", track.toAnilistStatus())
} }
} }
val body = payload.toString().toRequestBody(jsonMime) return authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime)))
val request = Request.Builder()
.url(apiUrl)
.post(body)
.build()
return authClient.newCall(request)
.asObservableSuccess() .asObservableSuccess()
.map { netResponse -> .map { netResponse ->
val responseBody = netResponse.body?.string().orEmpty() netResponse.use {
netResponse.close() val responseBody = it.body?.string().orEmpty()
if (responseBody.isEmpty()) { if (responseBody.isEmpty()) {
throw Exception("Null Response") throw Exception("Null Response")
}
val response = json.decodeFromString<JsonObject>(responseBody)
track.library_id = response["data"]!!.jsonObject["SaveMediaListEntry"]!!.jsonObject["id"]!!.jsonPrimitive.long
track
} }
val response = json.decodeFromString<JsonObject>(responseBody)
track.library_id = response["data"]!!.jsonObject["SaveMediaListEntry"]!!.jsonObject["id"]!!.jsonPrimitive.long
track
} }
} }
@@ -90,12 +86,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("score", track.score.toInt()) put("score", track.score.toInt())
} }
} }
val body = payload.toString().toRequestBody(jsonMime) return authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime)))
val request = Request.Builder()
.url(apiUrl)
.post(body)
.build()
return authClient.newCall(request)
.asObservableSuccess() .asObservableSuccess()
.map { .map {
track track
@@ -134,24 +125,21 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("query", search) put("query", search)
} }
} }
val body = payload.toString().toRequestBody(jsonMime) return authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime)))
val request = Request.Builder()
.url(apiUrl)
.post(body)
.build()
return authClient.newCall(request)
.asObservableSuccess() .asObservableSuccess()
.map { netResponse -> .map { netResponse ->
val responseBody = netResponse.body?.string().orEmpty() netResponse.use {
if (responseBody.isEmpty()) { val responseBody = it.body?.string().orEmpty()
throw Exception("Null Response") if (responseBody.isEmpty()) {
throw Exception("Null Response")
}
val response = json.decodeFromString<JsonObject>(responseBody)
val data = response["data"]!!.jsonObject
val page = data["Page"]!!.jsonObject
val media = page["media"]!!.jsonArray
val entries = media.map { jsonToALManga(it.jsonObject) }
entries.map { it.toTrack() }
} }
val response = json.decodeFromString<JsonObject>(responseBody)
val data = response["data"]!!.jsonObject
val page = data["Page"]!!.jsonObject
val media = page["media"]!!.jsonArray
val entries = media.map { jsonToALManga(it.jsonObject) }
entries.map { it.toTrack() }
} }
} }
@@ -194,24 +182,21 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("manga_id", track.media_id) put("manga_id", track.media_id)
} }
} }
val body = payload.toString().toRequestBody(jsonMime) return authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime)))
val request = Request.Builder()
.url(apiUrl)
.post(body)
.build()
return authClient.newCall(request)
.asObservableSuccess() .asObservableSuccess()
.map { netResponse -> .map { netResponse ->
val responseBody = netResponse.body?.string().orEmpty() netResponse.use {
if (responseBody.isEmpty()) { val responseBody = it.body?.string().orEmpty()
throw Exception("Null Response") if (responseBody.isEmpty()) {
throw Exception("Null Response")
}
val response = json.decodeFromString<JsonObject>(responseBody)
val data = response["data"]!!.jsonObject
val page = data["Page"]!!.jsonObject
val media = page["mediaList"]!!.jsonArray
val entries = media.map { jsonToALUserManga(it.jsonObject) }
entries.firstOrNull()?.toTrack()
} }
val response = json.decodeFromString<JsonObject>(responseBody)
val data = response["data"]!!.jsonObject
val page = data["Page"]!!.jsonObject
val media = page["mediaList"]!!.jsonArray
val entries = media.map { jsonToALUserManga(it.jsonObject) }
entries.firstOrNull()?.toTrack()
} }
} }
@@ -239,22 +224,22 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
val payload = buildJsonObject { val payload = buildJsonObject {
put("query", query) put("query", query)
} }
val body = payload.toString().toRequestBody(jsonMime) return authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime)))
val request = Request.Builder()
.url(apiUrl)
.post(body)
.build()
return authClient.newCall(request)
.asObservableSuccess() .asObservableSuccess()
.map { netResponse -> .map { netResponse ->
val responseBody = netResponse.body?.string().orEmpty() netResponse.use {
if (responseBody.isEmpty()) { val responseBody = it.body?.string().orEmpty()
throw Exception("Null Response") if (responseBody.isEmpty()) {
throw Exception("Null Response")
}
val response = json.decodeFromString<JsonObject>(responseBody)
val data = response["data"]!!.jsonObject
val viewer = data["Viewer"]!!.jsonObject
Pair(
viewer["id"]!!.jsonPrimitive.int,
viewer["mediaListOptions"]!!.jsonObject["scoreFormat"]!!.jsonPrimitive.content
)
} }
val response = json.decodeFromString<JsonObject>(responseBody)
val data = response["data"]!!.jsonObject
val viewer = data["Viewer"]!!.jsonObject
Pair(viewer["id"]!!.jsonPrimitive.int, viewer["mediaListOptions"]!!.jsonObject["scoreFormat"]!!.jsonPrimitive.content)
} }
} }
@@ -298,7 +283,6 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
companion object { companion object {
private const val clientId = "385" private const val clientId = "385"
private const val clientUrl = "tachiyomi://anilist-auth"
private const val apiUrl = "https://graphql.anilist.co/" private const val apiUrl = "https://graphql.anilist.co/"
private const val baseUrl = "https://anilist.co/api/v2/" private const val baseUrl = "https://anilist.co/api/v2/"
private const val baseMangaUrl = "https://anilist.co/manga/" private const val baseMangaUrl = "https://anilist.co/manga/"
@@ -63,7 +63,7 @@ data class ALUserManga(
"DROPPED" -> Anilist.DROPPED "DROPPED" -> Anilist.DROPPED
"PLANNING" -> Anilist.PLANNING "PLANNING" -> Anilist.PLANNING
"REPEATING" -> Anilist.REPEATING "REPEATING" -> Anilist.REPEATING
else -> throw NotImplementedError("Unknown status") else -> throw NotImplementedError("Unknown status: $list_status")
} }
} }
@@ -74,7 +74,7 @@ fun Track.toAnilistStatus() = when (status) {
Anilist.DROPPED -> "DROPPED" Anilist.DROPPED -> "DROPPED"
Anilist.PLANNING -> "PLANNING" Anilist.PLANNING -> "PLANNING"
Anilist.REPEATING -> "REPEATING" Anilist.REPEATING -> "REPEATING"
else -> throw NotImplementedError("Unknown status") else -> throw NotImplementedError("Unknown status: $status")
} }
private val preferences: PreferencesHelper by injectLazy() private val preferences: PreferencesHelper by injectLazy()
@@ -102,5 +102,5 @@ fun Track.toAnilistScore(): String = when (preferences.anilistScoreType().get())
} }
// 10 point decimal // 10 point decimal
"POINT_10_DECIMAL" -> (score / 10).toString() "POINT_10_DECIMAL" -> (score / 10).toString()
else -> throw Exception("Unknown score type") else -> throw NotImplementedError("Unknown score type")
} }
@@ -5,6 +5,7 @@ import androidx.core.net.toUri
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
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 kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
@@ -34,11 +35,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
.add("rating", track.score.toInt().toString()) .add("rating", track.score.toInt().toString())
.add("status", track.toBangumiStatus()) .add("status", track.toBangumiStatus())
.build() .build()
val request = Request.Builder() return authClient.newCall(POST("$apiUrl/collection/${track.media_id}/update", body = body))
.url("$apiUrl/collection/${track.media_id}/update")
.post(body)
.build()
return authClient.newCall(request)
.asObservableSuccess() .asObservableSuccess()
.map { .map {
track track
@@ -46,29 +43,20 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
} }
fun updateLibManga(track: Track): Observable<Track> { fun updateLibManga(track: Track): Observable<Track> {
// chapter update
val body = FormBody.Builder()
.add("watched_eps", track.last_chapter_read.toString())
.build()
val request = Request.Builder()
.url("$apiUrl/subject/${track.media_id}/update/watched_eps")
.post(body)
.build()
// read status update // read status update
val sbody = FormBody.Builder() val sbody = FormBody.Builder()
.add("status", track.toBangumiStatus()) .add("status", track.toBangumiStatus())
.build() .build()
val srequest = Request.Builder() return authClient.newCall(POST("$apiUrl/collection/${track.media_id}/update", body = sbody))
.url("$apiUrl/collection/${track.media_id}/update")
.post(sbody)
.build()
return authClient.newCall(srequest)
.asObservableSuccess() .asObservableSuccess()
.map { .map {
track track
}.flatMap { }.flatMap {
authClient.newCall(request) // chapter update
val body = FormBody.Builder()
.add("watched_eps", track.last_chapter_read.toString())
.build()
authClient.newCall(POST("$apiUrl/subject/${track.media_id}/update/watched_eps", body = body))
.asObservableSuccess() .asObservableSuccess()
.map { .map {
track track
@@ -82,11 +70,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
.buildUpon() .buildUpon()
.appendQueryParameter("max_results", "20") .appendQueryParameter("max_results", "20")
.build() .build()
val request = Request.Builder() return authClient.newCall(GET(url.toString()))
.url(url.toString())
.get()
.build()
return authClient.newCall(request)
.asObservableSuccess() .asObservableSuccess()
.map { netResponse -> .map { netResponse ->
var responseBody = netResponse.body?.string().orEmpty() var responseBody = netResponse.body?.string().orEmpty()
@@ -126,13 +110,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
} }
fun findLibManga(track: Track): Observable<Track?> { fun findLibManga(track: Track): Observable<Track?> {
val urlMangas = "$apiUrl/subject/${track.media_id}" return authClient.newCall(GET("$apiUrl/subject/${track.media_id}"))
val requestMangas = Request.Builder()
.url(urlMangas)
.get()
.build()
return authClient.newCall(requestMangas)
.asObservableSuccess() .asObservableSuccess()
.map { netResponse -> .map { netResponse ->
// get comic info // get comic info
@@ -162,13 +140,17 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
} }
fun accessToken(code: String): Observable<OAuth> { fun accessToken(code: String): Observable<OAuth> {
return client.newCall(accessTokenRequest(code)).asObservableSuccess().map { netResponse -> return client.newCall(accessTokenRequest(code))
val responseBody = netResponse.body?.string().orEmpty() .asObservableSuccess()
if (responseBody.isEmpty()) { .map { netResponse ->
throw Exception("Null Response") netResponse.use {
val responseBody = it.body?.string().orEmpty()
if (responseBody.isEmpty()) {
throw Exception("Null Response")
}
json.decodeFromString<OAuth>(responseBody)
}
} }
json.decodeFromString<OAuth>(responseBody)
}
} }
private fun accessTokenRequest(code: String) = POST( private fun accessTokenRequest(code: String) = POST(
@@ -8,7 +8,7 @@ fun Track.toBangumiStatus() = when (status) {
Bangumi.ON_HOLD -> "on_hold" Bangumi.ON_HOLD -> "on_hold"
Bangumi.DROPPED -> "dropped" Bangumi.DROPPED -> "dropped"
Bangumi.PLANNING -> "wish" Bangumi.PLANNING -> "wish"
else -> throw NotImplementedError("Unknown status") else -> throw NotImplementedError("Unknown status: $status")
} }
fun toTrackStatus(status: String) = when (status) { fun toTrackStatus(status: String) = when (status) {
@@ -17,6 +17,5 @@ fun toTrackStatus(status: String) = when (status) {
"on_hold" -> Bangumi.ON_HOLD "on_hold" -> Bangumi.ON_HOLD
"dropped" -> Bangumi.DROPPED "dropped" -> Bangumi.DROPPED
"wish" -> Bangumi.PLANNING "wish" -> Bangumi.PLANNING
else -> throw NotImplementedError("Unknown status: $status")
else -> throw Exception("Unknown status")
} }
@@ -111,7 +111,7 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
fun ensureLoggedIn() { fun ensureLoggedIn() {
if (isAuthorized) return if (isAuthorized) return
if (!isLogged) throw Exception("MAL login credentials not found") if (!isLogged) throw Exception(context.getString(R.string.myanimelist_creds_missing))
} }
override fun logout() { override fun logout() {
@@ -12,7 +12,7 @@ import eu.kanade.tachiyomi.util.lang.toCalendar
import eu.kanade.tachiyomi.util.selectInt import eu.kanade.tachiyomi.util.selectInt
import eu.kanade.tachiyomi.util.selectText import eu.kanade.tachiyomi.util.selectText
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.RequestBody import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
@@ -282,7 +282,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.put("score", track.score) .put("score", track.score)
.put("num_read_chapters", track.last_chapter_read) .put("num_read_chapters", track.last_chapter_read)
return body.toString().toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull()) return body.toString().toRequestBody("application/json; charset=utf-8".toMediaType())
} }
private fun mangaEditPostBody(track: MyAnimeListEditData): RequestBody { private fun mangaEditPostBody(track: MyAnimeListEditData): RequestBody {
@@ -19,9 +19,8 @@ import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonObject import kotlinx.serialization.json.putJsonObject
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import rx.Observable import rx.Observable
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@@ -30,7 +29,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
private val json: Json by injectLazy() private val json: Json by injectLazy()
private val jsonime = "application/json; charset=utf-8".toMediaTypeOrNull() private val jsonMime = "application/json; charset=utf-8".toMediaType()
private val authClient = client.newBuilder().addInterceptor(interceptor).build() private val authClient = client.newBuilder().addInterceptor(interceptor).build()
fun addLibManga(track: Track, user_id: String): Observable<Track> { fun addLibManga(track: Track, user_id: String): Observable<Track> {
@@ -44,12 +43,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
put("status", track.toShikimoriStatus()) put("status", track.toShikimoriStatus())
} }
} }
val body = payload.toString().toRequestBody(jsonime) return authClient.newCall(POST("$apiUrl/v2/user_rates", body = payload.toString().toRequestBody(jsonMime)))
val request = Request.Builder()
.url("$apiUrl/v2/user_rates")
.post(body)
.build()
return authClient.newCall(request)
.asObservableSuccess() .asObservableSuccess()
.map { .map {
track track
@@ -64,11 +58,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
.appendQueryParameter("search", search) .appendQueryParameter("search", search)
.appendQueryParameter("limit", "20") .appendQueryParameter("limit", "20")
.build() .build()
val request = Request.Builder() return authClient.newCall(GET(url.toString()))
.url(url.toString())
.get()
.build()
return authClient.newCall(request)
.asObservableSuccess() .asObservableSuccess()
.map { netResponse -> .map { netResponse ->
val responseBody = netResponse.body?.string().orEmpty() val responseBody = netResponse.body?.string().orEmpty()
@@ -107,30 +97,21 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
} }
fun findLibManga(track: Track, user_id: String): Observable<Track?> { fun findLibManga(track: Track, user_id: String): Observable<Track?> {
val url = "$apiUrl/v2/user_rates".toUri().buildUpon()
.appendQueryParameter("user_id", user_id)
.appendQueryParameter("target_id", track.media_id.toString())
.appendQueryParameter("target_type", "Manga")
.build()
val request = Request.Builder()
.url(url.toString())
.get()
.build()
val urlMangas = "$apiUrl/mangas".toUri().buildUpon() val urlMangas = "$apiUrl/mangas".toUri().buildUpon()
.appendPath(track.media_id.toString()) .appendPath(track.media_id.toString())
.build() .build()
val requestMangas = Request.Builder() return authClient.newCall(GET(urlMangas.toString()))
.url(urlMangas.toString())
.get()
.build()
return authClient.newCall(requestMangas)
.asObservableSuccess() .asObservableSuccess()
.map { netResponse -> .map { netResponse ->
val responseBody = netResponse.body?.string().orEmpty() val responseBody = netResponse.body?.string().orEmpty()
json.decodeFromString<JsonObject>(responseBody) json.decodeFromString<JsonObject>(responseBody)
}.flatMap { mangas -> }.flatMap { mangas ->
authClient.newCall(request) val url = "$apiUrl/v2/user_rates".toUri().buildUpon()
.appendQueryParameter("user_id", user_id)
.appendQueryParameter("target_id", track.media_id.toString())
.appendQueryParameter("target_type", "Manga")
.build()
authClient.newCall(GET(url.toString()))
.asObservableSuccess() .asObservableSuccess()
.map { netResponse -> .map { netResponse ->
val responseBody = netResponse.body?.string().orEmpty() val responseBody = netResponse.body?.string().orEmpty()
@@ -155,13 +136,17 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
} }
fun accessToken(code: String): Observable<OAuth> { fun accessToken(code: String): Observable<OAuth> {
return client.newCall(accessTokenRequest(code)).asObservableSuccess().map { netResponse -> return client.newCall(accessTokenRequest(code))
val responseBody = netResponse.body?.string().orEmpty() .asObservableSuccess()
if (responseBody.isEmpty()) { .map { netResponse ->
throw Exception("Null Response") netResponse.use {
val responseBody = it.body?.string().orEmpty()
if (responseBody.isEmpty()) {
throw Exception("Null Response")
}
json.decodeFromString<OAuth>(responseBody)
}
} }
json.decodeFromString<OAuth>(responseBody)
}
} }
private fun accessTokenRequest(code: String) = POST( private fun accessTokenRequest(code: String) = POST(
@@ -9,7 +9,7 @@ fun Track.toShikimoriStatus() = when (status) {
Shikimori.DROPPED -> "dropped" Shikimori.DROPPED -> "dropped"
Shikimori.PLANNING -> "planned" Shikimori.PLANNING -> "planned"
Shikimori.REPEATING -> "rewatching" Shikimori.REPEATING -> "rewatching"
else -> throw NotImplementedError("Unknown status") else -> throw NotImplementedError("Unknown status: $status")
} }
fun toTrackStatus(status: String) = when (status) { fun toTrackStatus(status: String) = when (status) {
@@ -19,6 +19,5 @@ fun toTrackStatus(status: String) = when (status) {
"dropped" -> Shikimori.DROPPED "dropped" -> Shikimori.DROPPED
"planned" -> Shikimori.PLANNING "planned" -> Shikimori.PLANNING
"rewatching" -> Shikimori.REPEATING "rewatching" -> Shikimori.REPEATING
else -> throw NotImplementedError("Unknown status: $status")
else -> throw Exception("Unknown status")
} }
@@ -37,11 +37,11 @@ class GithubUpdateChecker {
val newVersion = versionTag.replace("[^\\d.]".toRegex(), "") val newVersion = versionTag.replace("[^\\d.]".toRegex(), "")
return if (BuildConfig.DEBUG) { return if (BuildConfig.DEBUG) {
// Preview builds: based on releases in "tachiyomiorg/android-app-preview" repo // Preview builds: based on releases in "tachiyomiorg/tachiyomi-preview" repo
// tagged as something like "r1234" // tagged as something like "r1234"
newVersion.toInt() > BuildConfig.COMMIT_COUNT.toInt() newVersion.toInt() > BuildConfig.COMMIT_COUNT.toInt()
} else { } else {
// Release builds: based on releases in "inorichi/tachiyomi" repo // Release builds: based on releases in "tachiyomiorg/tachiyomi" repo
// tagged as something like "v0.1.2" // tagged as something like "v0.1.2"
newVersion != BuildConfig.VERSION_NAME newVersion != BuildConfig.VERSION_NAME
} }
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import androidx.core.content.ContextCompat
import com.elvishew.xlog.XLog import com.elvishew.xlog.XLog
import com.jakewharton.rxrelay.BehaviorRelay import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@@ -76,9 +77,9 @@ class ExtensionManager(
// SY --> // SY -->
return when (source.id) { return when (source.id) {
EH_SOURCE_ID -> context.getDrawable(R.mipmap.ic_ehentai_source) EH_SOURCE_ID -> ContextCompat.getDrawable(context, R.mipmap.ic_ehentai_source)
EXH_SOURCE_ID -> context.getDrawable(R.mipmap.ic_ehentai_source) EXH_SOURCE_ID -> ContextCompat.getDrawable(context, R.mipmap.ic_ehentai_source)
MERGED_SOURCE_ID -> context.getDrawable(R.mipmap.ic_merged_source) MERGED_SOURCE_ID -> ContextCompat.getDrawable(context, R.mipmap.ic_merged_source)
else -> null else -> null
} }
// SY <-- // SY <--
@@ -142,8 +143,7 @@ class ExtensionManager(
.map { it.extension } .map { it.extension }
installedExtensions installedExtensions
.flatMap { it.sources } .flatMap { it.sources }
// overwrite is needed until the bundled sources are removed .forEach { sourceManager.registerSource(it) }
.forEach { sourceManager.registerSource(it, true) }
untrustedExtensions = extensions untrustedExtensions = extensions
.filterIsInstance<LoadResult.Untrusted>() .filterIsInstance<LoadResult.Untrusted>()
@@ -25,7 +25,12 @@ internal class ExtensionGithubApi {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
val response = service.getRepo() val response = service.getRepo()
parseResponse(response) parseResponse(response)
} /* SY --> */ + preferences.extensionRepos().get().flatMap {
val url = "$BASE_URL$it/repo/"
val response = service.getRepo("${url}index.min.json")
parseResponse(response, url)
} }
// SY <--
} }
suspend fun checkForUpdates(context: Context): List<Extension.Installed> { suspend fun checkForUpdates(context: Context): List<Extension.Installed> {
@@ -58,7 +63,7 @@ internal class ExtensionGithubApi {
return extensionsWithUpdate return extensionsWithUpdate
} }
private fun parseResponse(json: JsonArray): List<Extension.Available> { private fun parseResponse(json: JsonArray /* SY --> */, repoUrl: String = REPO_URL_PREFIX /* SY <-- */): List<Extension.Available> {
return json return json
.filter { element -> .filter { element ->
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
@@ -73,20 +78,21 @@ internal class ExtensionGithubApi {
val versionCode = element.jsonObject["code"]!!.jsonPrimitive.int val versionCode = element.jsonObject["code"]!!.jsonPrimitive.int
val lang = element.jsonObject["lang"]!!.jsonPrimitive.content val lang = element.jsonObject["lang"]!!.jsonPrimitive.content
val nsfw = element.jsonObject["nsfw"]!!.jsonPrimitive.int == 1 val nsfw = element.jsonObject["nsfw"]!!.jsonPrimitive.int == 1
val icon = "$REPO_URL_PREFIX/icon/${apkName.replace(".apk", ".png")}" // SY -->
val icon = "$repoUrl/icon/${apkName.replace(".apk", ".png")}"
// SY <--
Extension.Available(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon) Extension.Available(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon /* SY --> */, repoUrl /* SY <-- */)
} }
} }
fun getApkUrl(extension: Extension.Available): String { fun getApkUrl(extension: Extension.Available): String {
return "$REPO_URL_PREFIX/apk/${extension.apkName}" return /* SY --> */ "${extension.repoUrl}/apk/${extension.apkName}" /* SY <-- */
} }
// SY --> // SY -->
fun Extension.isBlacklisted( private fun Extension.isBlacklisted(
blacklistEnabled: Boolean = blacklistEnabled: Boolean = preferences.enableSourceBlacklist().get()
preferences.enableSourceBlacklist().get()
): Boolean { ): Boolean {
return pkgName in BlacklistedSources.BLACKLISTED_EXTENSIONS && blacklistEnabled return pkgName in BlacklistedSources.BLACKLISTED_EXTENSIONS && blacklistEnabled
} }
@@ -94,6 +100,6 @@ internal class ExtensionGithubApi {
companion object { companion object {
const val BASE_URL = "https://raw.githubusercontent.com/" const val BASE_URL = "https://raw.githubusercontent.com/"
const val REPO_URL_PREFIX = "${BASE_URL}inorichi/tachiyomi-extensions/repo/" const val REPO_URL_PREFIX = "${BASE_URL}tachiyomiorg/tachiyomi-extensions/repo/"
} }
} }
@@ -7,6 +7,7 @@ import kotlinx.serialization.json.JsonArray
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Url
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
/** /**
@@ -27,6 +28,8 @@ interface ExtensionGithubService {
} }
} }
@GET("${ExtensionGithubApi.REPO_URL_PREFIX}index.min.json") // SY -->
suspend fun getRepo(): JsonArray @GET
suspend fun getRepo(@Url url: String = "${ExtensionGithubApi.REPO_URL_PREFIX}index.min.json"): JsonArray
// SY <--
} }
@@ -35,7 +35,10 @@ sealed class Extension {
override val lang: String, override val lang: String,
override val isNsfw: Boolean, override val isNsfw: Boolean,
val apkName: String, val apkName: String,
val iconUrl: String val iconUrl: String,
// SY -->
val repoUrl: String
// SY <--
) : Extension() ) : Extension()
data class Untrusted( data class Untrusted(
@@ -6,7 +6,6 @@ import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import dalvik.system.PathClassLoader import dalvik.system.PathClassLoader
import eu.kanade.tachiyomi.annotations.Nsfw import eu.kanade.tachiyomi.annotations.Nsfw
import eu.kanade.tachiyomi.data.preference.PreferenceValues
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult import eu.kanade.tachiyomi.extension.model.LoadResult
@@ -26,8 +25,8 @@ import uy.kohesive.injekt.injectLazy
internal object ExtensionLoader { internal object ExtensionLoader {
private val preferences: PreferencesHelper by injectLazy() private val preferences: PreferencesHelper by injectLazy()
private val allowNsfwSource by lazy { private val loadNsfwSource by lazy {
preferences.allowNsfwSource().get() preferences.showNsfwSource().get()
} }
private const val EXTENSION_FEATURE = "tachiyomi.extension" private const val EXTENSION_FEATURE = "tachiyomi.extension"
@@ -133,7 +132,7 @@ internal object ExtensionLoader {
} }
val isNsfw = appInfo.metaData.getInt(METADATA_NSFW) == 1 val isNsfw = appInfo.metaData.getInt(METADATA_NSFW) == 1
if (allowNsfwSource == PreferenceValues.NsfwAllowance.BLOCKED && isNsfw) { if (!loadNsfwSource && isNsfw) {
return LoadResult.Error("NSFW extension $pkgName not allowed") return LoadResult.Error("NSFW extension $pkgName not allowed")
} }
@@ -218,7 +217,7 @@ internal object ExtensionLoader {
* Checks whether a Source or SourceFactory is annotated with @Nsfw. * Checks whether a Source or SourceFactory is annotated with @Nsfw.
*/ */
private fun isSourceNsfw(clazz: Any): Boolean { private fun isSourceNsfw(clazz: Any): Boolean {
if (allowNsfwSource == PreferenceValues.NsfwAllowance.ALLOWED) { if (loadNsfwSource) {
return false return false
} }
@@ -62,14 +62,18 @@ interface Source : tachiyomi.source.Source {
/** /**
* [1.x API] Get the updated details for a manga. * [1.x API] Get the updated details for a manga.
*/ */
@Suppress("DEPRECATION")
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo { override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
return fetchMangaDetails(manga.toSManga()).awaitSingle() val sManga = manga.toSManga()
.toMangaInfo() val networkManga = fetchMangaDetails(sManga).awaitSingle()
sManga.copyFrom(networkManga)
return sManga.toMangaInfo()
} }
/** /**
* [1.x API] Get all the available chapters for a manga. * [1.x API] Get all the available chapters for a manga.
*/ */
@Suppress("DEPRECATION")
override suspend fun getChapterList(manga: MangaInfo): List<ChapterInfo> { override suspend fun getChapterList(manga: MangaInfo): List<ChapterInfo> {
return fetchChapterList(manga.toSManga()).awaitSingle() return fetchChapterList(manga.toSManga()).awaitSingle()
.map { it.toChapterInfo() } .map { it.toChapterInfo() }
@@ -78,6 +82,7 @@ interface Source : tachiyomi.source.Source {
/** /**
* [1.x API] Get the list of pages a chapter has. * [1.x API] Get the list of pages a chapter has.
*/ */
@Suppress("DEPRECATION")
override suspend fun getPageList(chapter: ChapterInfo): List<tachiyomi.source.model.Page> { override suspend fun getPageList(chapter: ChapterInfo): List<tachiyomi.source.model.Page> {
return fetchPageList(chapter.toSChapter()).awaitSingle() return fetchPageList(chapter.toSChapter()).awaitSingle()
.map { it.toPageUrl() } .map { it.toPageUrl() }
@@ -103,7 +103,7 @@ open class SourceManager(private val context: Context) {
} }
// SY <-- // SY <--
internal fun registerSource(source: Source, overwrite: Boolean = false) { internal fun registerSource(source: Source) {
// EXH --> // EXH -->
val sourceQName = source::class.qualifiedName val sourceQName = source::class.qualifiedName
val factories = DELEGATED_SOURCES.entries.filter { it.value.factory }.map { it.value.originalSourceQualifiedClassName } val factories = DELEGATED_SOURCES.entries.filter { it.value.factory }.map { it.value.originalSourceQualifiedClassName }
@@ -130,7 +130,7 @@ open class SourceManager(private val context: Context) {
} }
// EXH <-- // EXH <--
if (overwrite || !sourcesMap.containsKey(source.id)) { if (!sourcesMap.containsKey(source.id)) {
sourcesMap[source.id] = newSource sourcesMap[source.id] = newSource
} }
} }
@@ -69,10 +69,10 @@ class MergedSource : SuspendHttpSource() {
val mergedManga = db.getManga(manga.url, id).await() ?: throw Exception("merged manga not in db") val mergedManga = db.getManga(manga.url, id).await() ?: throw Exception("merged manga not in db")
val mangaReferences = mergedManga.id?.let { withContext(Dispatchers.IO) { db.getMergedMangaReferences(it).await() } } ?: throw Exception("merged manga id is null") val mangaReferences = mergedManga.id?.let { withContext(Dispatchers.IO) { db.getMergedMangaReferences(it).await() } } ?: throw Exception("merged manga id is null")
if (mangaReferences.isEmpty()) throw IllegalArgumentException("Manga references are empty, info unavailable, merge is likely corrupted") if (mangaReferences.isEmpty()) throw IllegalArgumentException("Manga references are empty, info unavailable, merge is likely corrupted")
if (mangaReferences.size == 1 || { if (mangaReferences.size == 1 || run {
val mangaReference = mangaReferences.firstOrNull() val mangaReference = mangaReferences.firstOrNull()
mangaReference == null || (mangaReference.mangaSourceId == MERGED_SOURCE_ID) mangaReference == null || (mangaReference.mangaSourceId == MERGED_SOURCE_ID)
}() }
) throw IllegalArgumentException("Manga references contain only the merged reference, merge is likely corrupted") ) throw IllegalArgumentException("Manga references contain only the merged reference, merge is likely corrupted")
emit( emit(
@@ -4,25 +4,15 @@ import android.content.res.Configuration
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
import eu.kanade.tachiyomi.util.system.LocaleHelper
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() { abstract class BaseThemedActivity : AppCompatActivity() {
val preferences: PreferencesHelper by injectLazy() val preferences: PreferencesHelper by injectLazy()
val scope = lifecycleScope
lateinit var binding: VB
@Suppress("LeakingThis")
private val secureActivityDelegate = SecureActivityDelegate(this)
private val isDarkMode: Boolean by lazy { private val isDarkMode: Boolean by lazy {
val themeMode = preferences.themeMode().get() val themeMode = preferences.themeMode().get()
(themeMode == Values.ThemeMode.dark) || (themeMode == Values.ThemeMode.dark) ||
@@ -63,11 +53,6 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
} }
} }
init {
@Suppress("LeakingThis")
LocaleHelper.updateConfiguration(this)
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
setTheme( setTheme(
when { when {
@@ -77,13 +62,5 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
) )
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
secureActivityDelegate.onCreate()
}
override fun onResume() {
super.onResume()
secureActivityDelegate.onResume()
} }
} }
@@ -0,0 +1,33 @@
package eu.kanade.tachiyomi.ui.base.activity
import android.os.Bundle
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
import eu.kanade.tachiyomi.util.system.LocaleHelper
abstract class BaseViewBindingActivity<VB : ViewBinding> : BaseThemedActivity() {
val scope = lifecycleScope
lateinit var binding: VB
@Suppress("LeakingThis")
private val secureActivityDelegate = SecureActivityDelegate(this)
init {
@Suppress("LeakingThis")
LocaleHelper.updateConfiguration(this)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
secureActivityDelegate.onCreate()
}
override fun onResume() {
super.onResume()
secureActivityDelegate.onResume()
}
}
@@ -12,7 +12,6 @@ import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType import com.bluelinelabs.conductor.ControllerChangeType
import com.bluelinelabs.conductor.RestoreViewOnCreateController import com.bluelinelabs.conductor.RestoreViewOnCreateController
import kotlinx.android.extensions.LayoutContainer import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.clearFindViewByIdCache
import timber.log.Timber import timber.log.Timber
abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) : abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
@@ -54,11 +53,6 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
return inflateView(inflater, container) return inflateView(inflater, container)
} }
override fun onDestroyView(view: View) {
super.onDestroyView(view)
clearFindViewByIdCache()
}
abstract fun inflateView(inflater: LayoutInflater, container: ViewGroup): View abstract fun inflateView(inflater: LayoutInflater, container: ViewGroup): View
open fun onViewCreated(view: View) {} open fun onViewCreated(view: View) {}
@@ -5,11 +5,14 @@ import android.view.View
import eu.davidea.viewholders.FlexibleViewHolder import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.ExtensionCardItemBinding import eu.kanade.tachiyomi.databinding.ExtensionCardItemBinding
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class ExtensionHolder(view: View, val adapter: ExtensionAdapter) : class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
@@ -17,6 +20,10 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
private val binding = ExtensionCardItemBinding.bind(view) private val binding = ExtensionCardItemBinding.bind(view)
private val shouldLabelNsfw by lazy {
Injekt.get<PreferencesHelper>().labelNsfwExtension()
}
init { init {
binding.extButton.setOnClickListener { binding.extButton.setOnClickListener {
adapter.buttonClickListener.onButtonClick(bindingAdapterPosition) adapter.buttonClickListener.onButtonClick(bindingAdapterPosition)
@@ -35,9 +42,9 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
extension is Extension.Installed && extension.isUnofficial -> itemView.context.getString(R.string.ext_unofficial) extension is Extension.Installed && extension.isUnofficial -> itemView.context.getString(R.string.ext_unofficial)
// SY --> // SY -->
extension is Extension.Installed && extension.isRedundant -> itemView.context.getString(R.string.ext_redundant) extension is Extension.Installed && extension.isRedundant -> itemView.context.getString(R.string.ext_redundant)
extension.isNsfw && shouldLabelNsfw -> itemView.context.getString(R.string.ext_nsfw_short).plusRepo(extension)
else -> "".plusRepo(extension)
// SY <-- // SY <--
extension.isNsfw -> itemView.context.getString(R.string.ext_nsfw_short)
else -> ""
}.toUpperCase() }.toUpperCase()
GlideApp.with(itemView.context).clear(binding.image) GlideApp.with(itemView.context).clear(binding.image)
@@ -51,6 +58,23 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
bindButton(item) bindButton(item)
} }
// SY -->
private fun String.plusRepo(extension: Extension): String {
return if (extension is Extension.Available) {
when (extension.repoUrl) {
ExtensionGithubApi.REPO_URL_PREFIX -> this
else -> {
this + if (this.isEmpty()) {
""
} else {
""
} + itemView.context.getString(R.string.repo_source)
}
}
} else this
}
// SY <--
@Suppress("ResourceType") @Suppress("ResourceType")
fun bindButton(item: ExtensionItem) = with(binding.extButton) { fun bindButton(item: ExtensionItem) = with(binding.extButton) {
isEnabled = true isEnabled = true
@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.browse.extension
import android.app.Application import android.app.Application
import android.os.Bundle import android.os.Bundle
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferenceValues
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
@@ -56,7 +55,7 @@ open class ExtensionPresenter(
private fun toItems(tuple: ExtensionTuple): List<ExtensionItem> { private fun toItems(tuple: ExtensionTuple): List<ExtensionItem> {
val context = Injekt.get<Application>() val context = Injekt.get<Application>()
val activeLangs = preferences.enabledLanguages().get() val activeLangs = preferences.enabledLanguages().get()
val showNsfwExtensions = preferences.allowNsfwSource().get() != PreferenceValues.NsfwAllowance.BLOCKED val showNsfwExtensions = preferences.showNsfwExtension().get()
val (installed, untrusted, available) = tuple val (installed, untrusted, available) = tuple
@@ -635,7 +635,6 @@ open class BrowseSourceController(bundle: Bundle) :
val adapter = adapter ?: return val adapter = adapter ?: return
preferences.sourceDisplayMode().set(mode) preferences.sourceDisplayMode().set(mode)
presenter.refreshDisplayMode()
activity?.invalidateOptionsMenu() activity?.invalidateOptionsMenu()
setupRecycler(view) setupRecycler(view)
@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.browse.source.browse
import android.os.Bundle import android.os.Bundle
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.flexibleadapter.items.ISectionable
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
@@ -38,10 +37,13 @@ import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.removeCovers import eu.kanade.tachiyomi.util.removeCovers
import exh.savedsearches.EXHSavedSearch import exh.savedsearches.EXHSavedSearch
import exh.savedsearches.JsonSavedSearch import exh.savedsearches.JsonSavedSearch
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.isActive
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
@@ -127,11 +129,6 @@ open class BrowseSourcePresenter(
private val filterSerializer = FilterSerializer() private val filterSerializer = FilterSerializer()
// SY <-- // SY <--
/**
* Job to initialize manga details.
*/
private var initializerJob: Job? = null
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
@@ -169,8 +166,6 @@ open class BrowseSourcePresenter(
this.query = query this.query = query
this.appliedFilters = filters this.appliedFilters = filters
initializeManga()
// Create a new pager. // Create a new pager.
pager = createPager(query, filters) pager = createPager(query, filters)
@@ -226,27 +221,6 @@ open class BrowseSourcePresenter(
return pager.hasNextPage return pager.hasNextPage
} }
/**
* Subscribes to the initializer of manga details and updates the view if needed.
*/
private fun initializeManga() {
initializerJob?.cancel()
initializerJob = launchIO {
mangaDetailsFlow
.onEach { mangas ->
if (!isActive) return@onEach
try {
mangas.filter { it.thumbnail_url == null && !it.initialized }
.map { getMangaDetails(it) }
.forEach { launchUI { view?.onMangaInitialized(it) } }
} catch (error: Exception) {
launchUI { Timber.e(error) }
}
}
}
}
/** /**
* Returns a manga from the database for the given manga from network. It creates a new entry * Returns a manga from the database for the given manga from network. It creates a new entry
* if the manga is not yet in the database. * if the manga is not yet in the database.
@@ -272,7 +246,19 @@ open class BrowseSourcePresenter(
* @param mangas the list of manga to initialize. * @param mangas the list of manga to initialize.
*/ */
fun initializeMangas(mangas: List<Manga>) { fun initializeMangas(mangas: List<Manga>) {
launchIO { mangaDetailsFlow.emit(mangas) } launchIO {
mangas.asFlow()
.filter { it.thumbnail_url == null && !it.initialized }
.map { getMangaDetails(it) }
.onEach {
launchUI {
@Suppress("DEPRECATION")
view?.onMangaInitialized(it)
}
}
.catch { e -> Timber.e(e) }
.collect()
}
} }
/** /**
@@ -282,17 +268,15 @@ open class BrowseSourcePresenter(
* @return the initialized manga * @return the initialized manga
*/ */
private suspend fun getMangaDetails(manga: Manga): Manga { private suspend fun getMangaDetails(manga: Manga): Manga {
return try { try {
source.getMangaDetails(manga.toMangaInfo()) val networkManga = source.getMangaDetails(manga.toMangaInfo())
.let { networkManga -> manga.copyFrom(networkManga.toSManga())
manga.copyFrom(networkManga.toSManga()) manga.initialized = true
manga.initialized = true db.insertManga(manga).executeAsBlocking()
db.insertManga(manga).executeAsBlocking()
manga
}
} catch (e: Exception) { } catch (e: Exception) {
manga Timber.e(e)
} }
return manga
} }
/** /**
@@ -316,13 +300,6 @@ open class BrowseSourcePresenter(
db.insertManga(manga).executeAsBlocking() db.insertManga(manga).executeAsBlocking()
} }
/**
* Refreshes the active display mode.
*/
fun refreshDisplayMode() {
initializeManga()
}
/** /**
* Set the filter states for the current source. * Set the filter states for the current source.
* *
@@ -363,7 +340,7 @@ open class BrowseSourcePresenter(
is Filter.AutoComplete -> AutoCompleteSectionItem(it) is Filter.AutoComplete -> AutoCompleteSectionItem(it)
// SY <-- // SY <--
else -> null else -> null
} as? ISectionable<*, *> }
} }
subItems.forEach { it.header = group } subItems.forEach { it.header = group }
group.subItems = subItems group.subItems = subItems
@@ -0,0 +1,30 @@
package eu.kanade.tachiyomi.ui.category.repos
import eu.davidea.flexibleadapter.FlexibleAdapter
/**
* Custom adapter for repos.
*
* @param controller The containing controller.
*/
class RepoAdapter(controller: RepoController) :
FlexibleAdapter<RepoItem>(null, controller, true) {
/**
* Clears the active selections from the list and the model.
*/
override fun clearSelection() {
super.clearSelection()
(0 until itemCount).forEach { getItem(it)?.isSelected = false }
}
/**
* Toggles the selection of the given position.
*
* @param position The position to toggle.
*/
override fun toggleSelection(position: Int) {
super.toggleSelection(position)
getItem(position)?.isSelected = isSelected(position)
}
}
@@ -0,0 +1,320 @@
package eu.kanade.tachiyomi.ui.category.repos
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.snackbar.Snackbar
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter
import eu.davidea.flexibleadapter.helpers.UndoHelper
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.CategoriesControllerBinding
import eu.kanade.tachiyomi.ui.base.controller.FabController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.shrinkOnScroll
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.android.view.clicks
/**
* Controller to manage the categories for the users' library.
*/
class RepoController :
NucleusController<CategoriesControllerBinding, RepoPresenter>(),
FabController,
ActionMode.Callback,
FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener,
RepoCreateDialog.Listener,
UndoHelper.OnActionListener {
/**
* Object used to show ActionMode toolbar.
*/
private var actionMode: ActionMode? = null
/**
* Adapter containing repo items.
*/
private var adapter: RepoAdapter? = null
private var actionFab: ExtendedFloatingActionButton? = null
private var actionFabScrollListener: RecyclerView.OnScrollListener? = null
/**
* Undo helper used for restoring a deleted repo.
*/
private var undoHelper: UndoHelper? = null
/**
* Creates the presenter for this controller. Not to be manually called.
*/
override fun createPresenter() = RepoPresenter()
/**
* Returns the toolbar title to show when this controller is attached.
*/
override fun getTitle(): String? {
return resources?.getString(R.string.action_edit_repos)
}
/**
* 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)
return binding.root
}
/**
* Called after view inflation. Used to initialize the view.
*
* @param view The view of this controller.
*/
override fun onViewCreated(view: View) {
super.onViewCreated(view)
adapter = RepoAdapter(this@RepoController)
binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.setHasFixedSize(true)
binding.recycler.adapter = adapter
adapter?.isPermanentDelete = false
actionFabScrollListener = actionFab?.shrinkOnScroll(binding.recycler)
}
override fun configureFab(fab: ExtendedFloatingActionButton) {
actionFab = fab
fab.setText(R.string.action_add)
fab.setIconResource(R.drawable.ic_add_24dp)
fab.clicks()
.onEach {
RepoCreateDialog(this@RepoController).showDialog(router, null)
}
.launchIn(scope)
}
override fun cleanupFab(fab: ExtendedFloatingActionButton) {
actionFabScrollListener?.let { binding.recycler.removeOnScrollListener(it) }
actionFab = null
}
/**
* Called when the view is being destroyed. Used to release references and remove callbacks.
*
* @param view The view of this controller.
*/
override fun onDestroyView(view: View) {
// Manually call callback to delete repos if required
undoHelper?.onDeleteConfirmed(Snackbar.Callback.DISMISS_EVENT_MANUAL)
undoHelper = null
actionMode = null
adapter = null
super.onDestroyView(view)
}
/**
* Called from the presenter when the repos are updated.
*
* @param repos The new list of repos to display.
*/
fun setRepos(repos: List<RepoItem>) {
actionMode?.finish()
adapter?.updateDataSet(repos)
if (repos.isNotEmpty()) {
binding.emptyView.hide()
val selected = repos.filter { it.isSelected }
if (selected.isNotEmpty()) {
selected.forEach { onItemLongClick(repos.indexOf(it)) }
}
} else {
binding.emptyView.show(R.string.information_empty_repos)
}
}
/**
* Called when action mode is first created. The menu supplied will be used to generate action
* buttons for the action mode.
*
* @param mode ActionMode being created.
* @param menu Menu used to populate action buttons.
* @return true if the action mode should be created, false if entering this mode should be
* aborted.
*/
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
// Inflate menu.
mode.menuInflater.inflate(R.menu.category_selection, menu)
// Enable adapter multi selection.
adapter?.mode = SelectableAdapter.Mode.MULTI
return true
}
/**
* Called to refresh an action mode's action menu whenever it is invalidated.
*
* @param mode ActionMode being prepared.
* @param menu Menu used to populate action buttons.
* @return true if the menu or action mode was updated, false otherwise.
*/
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
val adapter = adapter ?: return false
val count = adapter.selectedItemCount
mode.title = count.toString()
// Show edit button only when one item is selected
val editItem = mode.menu.findItem(R.id.action_edit)
editItem.isVisible = false
return true
}
/**
* Called to report a user click on an action button.
*
* @param mode The current ActionMode.
* @param item The item that was clicked.
* @return true if this callback handled the event, false if the standard MenuItem invocation
* should continue.
*/
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
val adapter = adapter ?: return false
when (item.itemId) {
R.id.action_delete -> {
undoHelper = UndoHelper(adapter, this)
undoHelper?.start(
adapter.selectedPositions,
(activity as? MainActivity)?.binding?.rootCoordinator!!,
R.string.snack_repo_deleted,
R.string.action_undo,
3000
)
mode.finish()
}
else -> return false
}
return true
}
/**
* Called when an action mode is about to be exited and destroyed.
*
* @param mode The current ActionMode being destroyed.
*/
override fun onDestroyActionMode(mode: ActionMode) {
// Reset adapter to single selection
adapter?.mode = SelectableAdapter.Mode.IDLE
adapter?.clearSelection()
actionMode = null
}
/**
* Called when an item in the list is clicked.
*
* @param position The position of the clicked item.
* @return true if this click should enable selection mode.
*/
override fun onItemClick(view: View, position: Int): Boolean {
// Check if action mode is initialized and selected item exist.
return if (actionMode != null && position != RecyclerView.NO_POSITION) {
toggleSelection(position)
true
} else {
false
}
}
/**
* Called when an item in the list is long clicked.
*
* @param position The position of the clicked item.
*/
override fun onItemLongClick(position: Int) {
val activity = activity as? AppCompatActivity ?: return
// Check if action mode is initialized.
if (actionMode == null) {
// Initialize action mode
actionMode = activity.startSupportActionMode(this)
}
// Set item as selected
toggleSelection(position)
}
/**
* Toggle the selection state of an item.
* If the item was the last one in the selection and is unselected, the ActionMode is finished.
*
* @param position The position of the item to toggle.
*/
private fun toggleSelection(position: Int) {
val adapter = adapter ?: return
// Mark the position selected
adapter.toggleSelection(position)
if (adapter.selectedItemCount == 0) {
actionMode?.finish()
} else {
actionMode?.invalidate()
}
}
/**
* Called when the undo action is clicked in the snackbar.
*
* @param action The action performed.
*/
override fun onActionCanceled(action: Int, positions: MutableList<Int>?) {
adapter?.restoreDeletedItems()
undoHelper = null
}
/**
* Called when the time to restore the items expires.
*
* @param action The action performed.
* @param event The event that triggered the action
*/
override fun onActionConfirmed(action: Int, event: Int) {
val adapter = adapter ?: return
presenter.deleteRepos(adapter.deletedItems.map { it.repo })
undoHelper = null
}
/**
* Creates a new repo with the given name.
*
* @param name The name of the new repo.
*/
override fun createRepo(name: String) {
presenter.createRepo(name)
}
/**
* Called from the presenter when a repo already exists.
*/
fun onRepoExistsError() {
activity?.toast(R.string.error_repo_exists)
}
/**
* Called from the presenter when a invalid repo is made
*/
fun onRepoInvalidNameError() {
activity?.toast(R.string.invalid_repo_name)
}
}
@@ -0,0 +1,51 @@
package eu.kanade.tachiyomi.ui.category.repos
import android.app.Dialog
import android.os.Bundle
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.input.input
import com.bluelinelabs.conductor.Controller
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.DialogController
/**
* Dialog to create a new repo for the library.
*/
class RepoCreateDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
where T : Controller, T : RepoCreateDialog.Listener {
/**
* Name of the new repo. Value updated with each input from the user.
*/
private var currentName = ""
constructor(target: T) : this() {
targetController = target
}
/**
* Called when creating the dialog for this controller.
*
* @param savedViewState The saved state of this dialog.
* @return a new dialog instance.
*/
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
return MaterialDialog(activity!!)
.title(R.string.action_add_repo)
.message(R.string.action_add_repo_message)
.negativeButton(android.R.string.cancel)
.input(
hint = resources?.getString(R.string.name),
prefill = currentName
) { _, input ->
currentName = input.toString()
}
.positiveButton(android.R.string.ok) {
(targetController as? Listener)?.createRepo(currentName)
}
}
interface Listener {
fun createRepo(name: String)
}
}
@@ -0,0 +1,28 @@
package eu.kanade.tachiyomi.ui.category.repos
import android.view.View
import androidx.core.view.isVisible
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.databinding.CategoriesItemBinding
/**
* Holder used to display repo items.
*
* @param view The view used by repo items.
* @param adapter The adapter containing this holder.
*/
class RepoHolder(view: View, val adapter: RepoAdapter) : FlexibleViewHolder(view, adapter) {
private val binding = CategoriesItemBinding.bind(view)
/**
* Binds this holder with the given category.
*
* @param category The category to bind.
*/
fun bind(category: String) {
// Set capitalized title.
binding.title.text = category
binding.reorder.isVisible = false
}
}
@@ -0,0 +1,62 @@
package eu.kanade.tachiyomi.ui.category.repos
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
/**
* Repo item for a recycler view.
*/
class RepoItem(val repo: String) : AbstractFlexibleItem<RepoHolder>() {
/**
* Whether this item is currently selected.
*/
var isSelected = false
/**
* Returns the layout resource for this item.
*/
override fun getLayoutRes(): Int {
return R.layout.categories_item
}
/**
* Returns a new view holder for this item.
*
* @param view The view of this item.
* @param adapter The adapter of this item.
*/
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): RepoHolder {
return RepoHolder(view, adapter as RepoAdapter)
}
/**
* Binds the given view holder with this item.
*
* @param adapter The adapter of this item.
* @param holder The holder to bind.
* @param position The position of this item in the adapter.
* @param payloads List of partial changes.
*/
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
holder: RepoHolder,
position: Int,
payloads: List<Any?>?
) {
holder.bind(repo)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
return false
}
override fun hashCode(): Int {
return repo.hashCode()
}
}
@@ -0,0 +1,89 @@
package eu.kanade.tachiyomi.ui.category.repos
import android.os.Bundle
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* Presenter of [RepoController]. Used to manage the repos for the extensions.
*/
class RepoPresenter(
private val preferences: PreferencesHelper = Injekt.get()
) : BasePresenter<RepoController>() {
val scope = CoroutineScope(Job() + Dispatchers.Main)
/**
* List containing repos.
*/
private var repos: List<String> = emptyList()
/**
* Called when the presenter is created.
*
* @param savedState The saved state of this presenter.
*/
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
preferences.extensionRepos().asFlow().onEach { repos ->
this.repos = repos.toList().sortedBy { it.toLowerCase() }
Observable.just(this.repos)
.map { it.map(::RepoItem) }
.observeOn(AndroidSchedulers.mainThread())
.subscribeLatestCache(RepoController::setRepos)
}.launchIn(scope)
}
/**
* Creates and adds a new repo to the database.
*
* @param name The name of the repo to create.
*/
fun createRepo(name: String) {
// Do not allow duplicate repos.
if (repoExists(name)) {
Observable.just(Unit).subscribeFirst({ view, _ -> view.onRepoExistsError() })
return
}
// Do not allow invalid formats
if (!name.matches(repoRegex)) {
Observable.just(Unit).subscribeFirst({ view, _ -> view.onRepoInvalidNameError() })
return
}
preferences.extensionRepos().set((repos + name).toSet())
}
/**
* Deletes the given repos from the database.
*
* @param repos The list of repos to delete.
*/
fun deleteRepos(repos: List<String>) {
preferences.extensionRepos().set(
this.repos.filterNot { it in repos }.toSet()
)
}
/**
* Returns true if a repo with the given name already exists.
*/
private fun repoExists(name: String): Boolean {
return repos.any { it.equals(name, true) }
}
companion object {
val repoRegex = """^[a-zA-Z-_.]*?\/[a-zA-Z-_.]*?$""".toRegex()
}
}
@@ -1,9 +1,10 @@
package eu.kanade.tachiyomi.ui.library package eu.kanade.tachiyomi.ui.library
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.databinding.LibraryCategoryBinding
import eu.kanade.tachiyomi.util.view.inflate import eu.kanade.tachiyomi.util.view.inflate
import eu.kanade.tachiyomi.widget.RecyclerViewPagerAdapter import eu.kanade.tachiyomi.widget.RecyclerViewPagerAdapter
@@ -34,8 +35,9 @@ class LibraryAdapter(private val controller: LibraryController) : RecyclerViewPa
* @return a new view. * @return a new view.
*/ */
override fun createView(container: ViewGroup): View { override fun createView(container: ViewGroup): View {
val view = container.inflate(R.layout.library_category) as LibraryCategoryView val binding = LibraryCategoryBinding.inflate(LayoutInflater.from(container.context), container, false)
view.onCreate(controller) val view: LibraryCategoryView = binding.root
view.onCreate(controller, binding)
return view return view
} }
@@ -16,14 +16,13 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateService
import eu.kanade.tachiyomi.data.preference.PreferenceValues import eu.kanade.tachiyomi.data.preference.PreferenceValues
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.LibraryCategoryBinding
import eu.kanade.tachiyomi.ui.category.CategoryAdapter import eu.kanade.tachiyomi.ui.category.CategoryAdapter
import eu.kanade.tachiyomi.util.lang.plusAssign import eu.kanade.tachiyomi.util.lang.plusAssign
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.inflate import eu.kanade.tachiyomi.util.view.inflate
import eu.kanade.tachiyomi.widget.AutofitRecyclerView import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import exh.ui.LoadingHandle import exh.ui.LoadingHandle
import kotlinx.android.synthetic.main.library_category.view.fast_scroller
import kotlinx.android.synthetic.main.library_category.view.swipe_refresh
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -96,15 +95,15 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
} }
// EXH <-- // EXH <--
fun onCreate(controller: LibraryController) { fun onCreate(controller: LibraryController, binding: LibraryCategoryBinding) {
this.controller = controller this.controller = controller
recycler = if (preferences.libraryDisplayMode().get() == DisplayMode.LIST) { recycler = if (preferences.libraryDisplayMode().get() == DisplayMode.LIST) {
(swipe_refresh.inflate(R.layout.library_list_recycler) as RecyclerView).apply { (binding.swipeRefresh.inflate(R.layout.library_list_recycler) as RecyclerView).apply {
layoutManager = LinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
} }
} else { } else {
(swipe_refresh.inflate(R.layout.library_grid_recycler) as AutofitRecyclerView).apply { (binding.swipeRefresh.inflate(R.layout.library_grid_recycler) as AutofitRecyclerView).apply {
spanCount = controller.mangaPerRow spanCount = controller.mangaPerRow
} }
} }
@@ -113,21 +112,21 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
recycler.setHasFixedSize(true) recycler.setHasFixedSize(true)
recycler.adapter = adapter recycler.adapter = adapter
swipe_refresh.addView(recycler) binding.swipeRefresh.addView(recycler)
adapter.fastScroller = fast_scroller adapter.fastScroller = binding.fastScroller
recycler.scrollStateChanges() recycler.scrollStateChanges()
.onEach { .onEach {
// Disable swipe refresh when view is not at the top // Disable swipe refresh when view is not at the top
val firstPos = (recycler.layoutManager as LinearLayoutManager) val firstPos = (recycler.layoutManager as LinearLayoutManager)
.findFirstCompletelyVisibleItemPosition() .findFirstCompletelyVisibleItemPosition()
swipe_refresh.isEnabled = firstPos <= 0 binding.swipeRefresh.isEnabled = firstPos <= 0
} }
.launchIn(scope) .launchIn(scope)
// Double the distance required to trigger sync // Double the distance required to trigger sync
swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt()) binding.swipeRefresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt())
swipe_refresh.refreshes() binding.swipeRefresh.refreshes()
.onEach { .onEach {
// SY --> // SY -->
if (LibraryUpdateService.start(context, if (controller.presenter.groupType == LibraryGroup.BY_DEFAULT) category else null, group = controller.presenter.groupType, groupExtra = getGroupExtra())) { if (LibraryUpdateService.start(context, if (controller.presenter.groupType == LibraryGroup.BY_DEFAULT) category else null, group = controller.presenter.groupType, groupExtra = getGroupExtra())) {
@@ -147,7 +146,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
// SY <-- // SY <--
// It can be a very long operation, so we disable swipe refresh and show a toast. // It can be a very long operation, so we disable swipe refresh and show a toast.
swipe_refresh.isRefreshing = false binding.swipeRefresh.isRefreshing = false
} }
.launchIn(scope) .launchIn(scope)
} }
@@ -174,7 +174,7 @@ class LibraryController(
if (preferences.categoryTabs().get()) { if (preferences.categoryTabs().get()) {
currentTitle = resources?.getString(R.string.label_library) currentTitle = resources?.getString(R.string.label_library)
} else { } else {
adapter?.categories?.get(binding.libraryPager.currentItem)?.let { adapter?.categories?.getOrNull(binding.libraryPager.currentItem)?.let {
currentTitle = it.name currentTitle = it.name
} }
} }
@@ -684,7 +684,7 @@ class LibraryPresenter(
libraryManga.forEach { libraryItem -> libraryManga.forEach { libraryItem ->
when (groupType) { when (groupType) {
LibraryGroup.BY_TRACK_STATUS -> { LibraryGroup.BY_TRACK_STATUS -> {
val status: String = { val status: String = run {
val tracks = db.getTracks(libraryItem.manga).executeAsBlocking() val tracks = db.getTracks(libraryItem.manga).executeAsBlocking()
val track = tracks.find { track -> val track = tracks.find { track ->
loggedServices.any { it.id == track?.sync_id } loggedServices.any { it.id == track?.sync_id }
@@ -695,30 +695,30 @@ class LibraryPresenter(
} else { } else {
"not tracked" "not tracked"
} }
}() }
val group = grouping.find { it.first == trackManager.mapTrackingOrder(status, context).toString() } val group = grouping.find { it.first == trackManager.mapTrackingOrder(status, context).toString() }
if (group != null) { if (group != null) {
map[group.second]?.plusAssign(libraryItem) ?: map.put(group.second, mutableListOf(libraryItem)) map.getOrPut(group.second) { mutableListOf() } += libraryItem
} else { } else {
map[7]?.plusAssign(libraryItem) ?: map.put(7, mutableListOf(libraryItem)) map.getOrPut(7) { mutableListOf() } += libraryItem
} }
} }
LibraryGroup.BY_SOURCE -> { LibraryGroup.BY_SOURCE -> {
val group = grouping.find { it.first.toLongOrNull() == libraryItem.manga.source } val group = grouping.find { it.first.toLongOrNull() == libraryItem.manga.source }
if (group != null) { if (group != null) {
map[group.second]?.plusAssign(libraryItem) ?: map.put(group.second, mutableListOf(libraryItem)) map.getOrPut(group.second) { mutableListOf() } += libraryItem
} else { } else {
if (grouping.all { it.second != Int.MAX_VALUE }) grouping += Triple(Int.MAX_VALUE.toString(), Int.MAX_VALUE, context.getString(R.string.unknown)) if (grouping.all { it.second != Int.MAX_VALUE }) grouping += Triple(Int.MAX_VALUE.toString(), Int.MAX_VALUE, context.getString(R.string.unknown))
map[Int.MAX_VALUE]?.plusAssign(libraryItem) ?: map.put(Int.MAX_VALUE, mutableListOf(libraryItem)) map.getOrPut(Int.MAX_VALUE) { mutableListOf() } += libraryItem
} }
} }
else -> { else -> {
val group = grouping.find { it.second == libraryItem.manga.status } val group = grouping.find { it.second == libraryItem.manga.status }
if (group != null) { if (group != null) {
map[group.second]?.plusAssign(libraryItem) ?: map.put(group.second, mutableListOf(libraryItem)) map.getOrPut(group.second) { mutableListOf() } += libraryItem
} else { } else {
if (grouping.all { it.second != Int.MAX_VALUE }) grouping += Triple(Int.MAX_VALUE.toString(), Int.MAX_VALUE, context.getString(R.string.unknown)) if (grouping.all { it.second != Int.MAX_VALUE }) grouping += Triple(Int.MAX_VALUE.toString(), Int.MAX_VALUE, context.getString(R.string.unknown))
map[Int.MAX_VALUE]?.plusAssign(libraryItem) ?: map.put(Int.MAX_VALUE, mutableListOf(libraryItem)) map.getOrPut(Int.MAX_VALUE) { mutableListOf() } += libraryItem
} }
} }
} }
@@ -730,11 +730,12 @@ class LibraryPresenter(
LibraryGroup.BY_TRACK_STATUS, LibraryGroup.BY_STATUS -> grouping.filter { it.second in map.keys } LibraryGroup.BY_TRACK_STATUS, LibraryGroup.BY_STATUS -> grouping.filter { it.second in map.keys }
else -> grouping else -> grouping
} }
).map { )
val category = Category.create(it.third) .map {
category.id = it.second val category = Category.create(it.third)
category category.id = it.second
} category
}
return map to categories return map to categories
} }
@@ -23,7 +23,7 @@ import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.preference.asImmediateFlow import eu.kanade.tachiyomi.data.preference.asImmediateFlow
import eu.kanade.tachiyomi.databinding.MainActivityBinding import eu.kanade.tachiyomi.databinding.MainActivityBinding
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity import eu.kanade.tachiyomi.ui.base.activity.BaseViewBindingActivity
import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.FabController import eu.kanade.tachiyomi.ui.base.controller.FabController
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
@@ -54,7 +54,7 @@ import java.util.Date
import java.util.LinkedList import java.util.LinkedList
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class MainActivity : BaseActivity<MainActivityBinding>() { class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
private lateinit var router: Router private lateinit var router: Router
@@ -62,17 +62,13 @@ class EditMangaDialog : DialogController {
} }
override fun onCreateDialog(savedViewState: Bundle?): Dialog { override fun onCreateDialog(savedViewState: Bundle?): Dialog {
binding = EditMangaDialogBinding.inflate(activity!!.layoutInflater)
val dialog = MaterialDialog(activity!!).apply { val dialog = MaterialDialog(activity!!).apply {
customView(viewRes = R.layout.edit_manga_dialog, scrollable = true) customView(view = binding.root, scrollable = true)
negativeButton(android.R.string.cancel) negativeButton(android.R.string.cancel)
positiveButton(R.string.action_save) { onPositiveButtonClick() } positiveButton(R.string.action_save) { onPositiveButtonClick() }
} }
binding = EditMangaDialogBinding.bind(dialog.view.contentLayout.customView!!)
onViewCreated() onViewCreated()
dialog.setOnShowListener {
val dView = (it as? MaterialDialog)?.view
dView?.contentLayout?.scrollView?.scrollTo(0, 0)
}
return dialog return dialog
} }
@@ -1333,7 +1333,7 @@ class MangaController :
private fun markPreviousAsRead(chapters: List<ChapterItem>) { private fun markPreviousAsRead(chapters: List<ChapterItem>) {
val adapter = chaptersAdapter ?: return val adapter = chaptersAdapter ?: return
val prevChapters = if (presenter.sortDescending()) adapter.items.reversed() else adapter.items val prevChapters = if (presenter.sortDescending()) adapter.items.reversed() else adapter.items
val chapterPos = prevChapters.indexOf(chapters.last()) val chapterPos = prevChapters.indexOf(chapters.lastOrNull())
if (chapterPos != -1) { if (chapterPos != -1) {
markAsRead(prevChapters.take(chapterPos)) markAsRead(prevChapters.take(chapterPos))
} }
@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.manga.merged
import android.app.Dialog import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.view.View
import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
@@ -19,9 +18,6 @@ import exh.merged.sql.models.MergedMangaReference
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
class EditMergedSettingsDialog : DialogController, EditMergedMangaAdapter.EditMergedMangaItemListener { class EditMergedSettingsDialog : DialogController, EditMergedMangaAdapter.EditMergedMangaItemListener {
private var dialogView: View? = null
private val manga: Manga private val manga: Manga
val mergedMangas: MutableList<Pair<Manga?, MergedMangaReference>> = mutableListOf() val mergedMangas: MutableList<Pair<Manga?, MergedMangaReference>> = mutableListOf()
@@ -55,22 +51,17 @@ class EditMergedSettingsDialog : DialogController, EditMergedMangaAdapter.EditMe
private var mergedMangaAdapter: EditMergedMangaAdapter? = null private var mergedMangaAdapter: EditMergedMangaAdapter? = null
override fun onCreateDialog(savedViewState: Bundle?): Dialog { override fun onCreateDialog(savedViewState: Bundle?): Dialog {
val dialog = MaterialDialog(activity!!).apply { binding = EditMergedSettingsDialogBinding.inflate(activity!!.layoutInflater)
customView(viewRes = R.layout.edit_merged_settings_dialog, scrollable = true) val dialog = MaterialDialog(activity!!)
negativeButton(android.R.string.cancel) .customView(view = binding.root, scrollable = true)
positiveButton(R.string.action_save) { onPositiveButtonClick() } .negativeButton(android.R.string.cancel)
} .positiveButton(R.string.action_save) { onPositiveButtonClick() }
dialogView = dialog.view
onViewCreated(dialog.view) onViewCreated()
dialog.setOnShowListener {
val dView = (it as? MaterialDialog)?.view
dView?.contentLayout?.scrollView?.scrollTo(0, 0)
}
return dialog return dialog
} }
fun onViewCreated(view: View) { fun onViewCreated() {
binding = EditMergedSettingsDialogBinding.bind(view)
val mergedManga = db.getMergedMangas(manga.id!!).executeAsBlocking() val mergedManga = db.getMergedMangas(manga.id!!).executeAsBlocking()
val mergedReferences = db.getMergedMangaReferences(manga.id!!).executeAsBlocking() val mergedReferences = db.getMergedMangaReferences(manga.id!!).executeAsBlocking()
if (mergedReferences.isEmpty() || mergedReferences.size == 1) { if (mergedReferences.isEmpty() || mergedReferences.size == 1) {
@@ -86,18 +77,13 @@ class EditMergedSettingsDialog : DialogController, EditMergedMangaAdapter.EditMe
mergedHeaderAdapter = EditMergedSettingsHeaderAdapter(this, mergedMangaAdapter!!) mergedHeaderAdapter = EditMergedSettingsHeaderAdapter(this, mergedMangaAdapter!!)
binding.recycler.adapter = ConcatAdapter(mergedHeaderAdapter, mergedMangaAdapter) binding.recycler.adapter = ConcatAdapter(mergedHeaderAdapter, mergedMangaAdapter)
binding.recycler.layoutManager = LinearLayoutManager(view.context) binding.recycler.layoutManager = LinearLayoutManager(activity!!)
mergedMangaAdapter?.isHandleDragEnabled = isPriorityOrder mergedMangaAdapter?.isHandleDragEnabled = isPriorityOrder
mergedMangaAdapter?.updateDataSet(mergedMangas.map { it.toModel() }) mergedMangaAdapter?.updateDataSet(mergedMangas.map { it.toModel() })
} }
override fun onDestroyView(view: View) {
super.onDestroyView(view)
dialogView = null
}
private fun onPositiveButtonClick() { private fun onPositiveButtonClick() {
mangaController.presenter.updateMergeSettings(mergeReference, mergedMangas.map { it.second }) mangaController.presenter.updateMergeSettings(mergeReference, mergedMangas.map { it.second })
} }
@@ -115,7 +101,7 @@ class EditMergedSettingsDialog : DialogController, EditMergedMangaAdapter.EditMe
val mergedMangaAdapter = mergedMangaAdapter ?: return val mergedMangaAdapter = mergedMangaAdapter ?: return
val mergeMangaReference = mergedMangaAdapter.currentItems.getOrNull(position)?.mergedMangaReference ?: return val mergeMangaReference = mergedMangaAdapter.currentItems.getOrNull(position)?.mergedMangaReference ?: return
MaterialDialog(dialogView!!.context) MaterialDialog(activity!!)
.title(R.string.delete_merged_manga) .title(R.string.delete_merged_manga)
.message(R.string.delete_merged_manga_desc) .message(R.string.delete_merged_manga_desc)
.positiveButton(android.R.string.ok) { .positiveButton(android.R.string.ok) {
@@ -128,7 +114,7 @@ class EditMergedSettingsDialog : DialogController, EditMergedMangaAdapter.EditMe
} }
override fun onToggleChapterUpdatesClicked(position: Int) { override fun onToggleChapterUpdatesClicked(position: Int) {
MaterialDialog(dialogView!!.context) MaterialDialog(activity!!)
.title(R.string.chapter_updates_merged_manga) .title(R.string.chapter_updates_merged_manga)
.message(R.string.chapter_updates_merged_manga_desc) .message(R.string.chapter_updates_merged_manga_desc)
.positiveButton(android.R.string.ok) { .positiveButton(android.R.string.ok) {
@@ -152,7 +138,7 @@ class EditMergedSettingsDialog : DialogController, EditMergedMangaAdapter.EditMe
} }
override fun onToggleChapterDownloadsClicked(position: Int) { override fun onToggleChapterDownloadsClicked(position: Int) {
MaterialDialog(dialogView!!.context) MaterialDialog(activity!!)
.title(R.string.download_merged_manga) .title(R.string.download_merged_manga)
.message(R.string.download_merged_manga_desc) .message(R.string.download_merged_manga_desc)
.positiveButton(android.R.string.ok) { .positiveButton(android.R.string.ok) {
@@ -42,7 +42,7 @@ class EditMergedSettingsHeaderAdapter(private val controller: EditMergedSettings
android.R.layout.simple_spinner_item, android.R.layout.simple_spinner_item,
listOf( listOf(
"No dedupe", "No dedupe",
"Dedupe by priority", /*"Dedupe by priority",*/
"Show source with most chapters", "Show source with most chapters",
"Show source with highest chapter number" "Show source with highest chapter number"
) )
@@ -54,8 +54,8 @@ class EditMergedSettingsHeaderAdapter(private val controller: EditMergedSettings
when (it.chapterSortMode) { when (it.chapterSortMode) {
MergedMangaReference.CHAPTER_SORT_NO_DEDUPE -> 0 MergedMangaReference.CHAPTER_SORT_NO_DEDUPE -> 0
MergedMangaReference.CHAPTER_SORT_PRIORITY -> 1 MergedMangaReference.CHAPTER_SORT_PRIORITY -> 1
MergedMangaReference.CHAPTER_SORT_MOST_CHAPTERS -> 2 MergedMangaReference.CHAPTER_SORT_MOST_CHAPTERS -> 1
MergedMangaReference.CHAPTER_SORT_HIGHEST_CHAPTER_NUMBER -> 3 MergedMangaReference.CHAPTER_SORT_HIGHEST_CHAPTER_NUMBER -> 2
else -> 0 else -> 0
} }
) )
@@ -69,9 +69,9 @@ class EditMergedSettingsHeaderAdapter(private val controller: EditMergedSettings
) { ) {
controller.mergeReference?.chapterSortMode = when (position) { controller.mergeReference?.chapterSortMode = when (position) {
0 -> MergedMangaReference.CHAPTER_SORT_NO_DEDUPE 0 -> MergedMangaReference.CHAPTER_SORT_NO_DEDUPE
1 -> MergedMangaReference.CHAPTER_SORT_PRIORITY 99 -> MergedMangaReference.CHAPTER_SORT_PRIORITY
2 -> MergedMangaReference.CHAPTER_SORT_MOST_CHAPTERS 1 -> MergedMangaReference.CHAPTER_SORT_MOST_CHAPTERS
3 -> MergedMangaReference.CHAPTER_SORT_HIGHEST_CHAPTER_NUMBER 2 -> MergedMangaReference.CHAPTER_SORT_HIGHEST_CHAPTER_NUMBER
else -> MergedMangaReference.CHAPTER_SORT_NO_DEDUPE else -> MergedMangaReference.CHAPTER_SORT_NO_DEDUPE
} }
XLog.d(controller.mergeReference?.chapterSortMode) XLog.d(controller.mergeReference?.chapterSortMode)
@@ -35,9 +35,6 @@ import java.util.TimeZone
class AboutController : SettingsController() { class AboutController : SettingsController() {
/**
* Checks for new releases
*/
private val updateChecker by lazy { GithubUpdateChecker() } private val updateChecker by lazy { GithubUpdateChecker() }
private val dateFormat: DateFormat = preferences.dateFormat() private val dateFormat: DateFormat = preferences.dateFormat()
@@ -119,7 +116,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 "
val url = "https://github.com/inorichi/tachiyomi" val url = "https://github.com/tachiyomiorg/tachiyomi"
summary = url summary = url
onClick { onClick {
val intent = Intent(Intent.ACTION_VIEW, url.toUri()) val intent = Intent(Intent.ACTION_VIEW, url.toUri())
@@ -130,7 +127,7 @@ class AboutController : SettingsController() {
preference { preference {
key = "pref_about_label_extensions" key = "pref_about_label_extensions"
titleRes = R.string.label_extensions titleRes = R.string.label_extensions
val url = "https://github.com/inorichi/tachiyomi-extensions" val url = "https://github.com/tachiyomiorg/tachiyomi-extensions"
summary = url summary = url
onClick { onClick {
val intent = Intent(Intent.ACTION_VIEW, url.toUri()) val intent = Intent(Intent.ACTION_VIEW, url.toUri())
@@ -105,6 +105,7 @@ class ReaderSettingsSheet(private val activity: ReaderActivity) : BottomSheetDia
binding.webtoonSidePadding.bindToIntPreference(preferences.webtoonSidePadding(), R.array.webtoon_side_padding_values) binding.webtoonSidePadding.bindToIntPreference(preferences.webtoonSidePadding(), R.array.webtoon_side_padding_values)
// SY --> // SY -->
binding.zoomOutWebtoon.bindToPreference(preferences.webtoonEnableZoomOut()) binding.zoomOutWebtoon.bindToPreference(preferences.webtoonEnableZoomOut())
binding.cropBordersContinuesVertical.bindToPreference(preferences.cropBordersContinuesVertical())
// SY <-- // SY <--
} }
@@ -20,7 +20,7 @@ class DirectoryPageLoader(val file: File) : PageLoader() {
override fun getPages(): Observable<List<ReaderPage>> { override fun getPages(): Observable<List<ReaderPage>> {
return file.listFiles() return file.listFiles()
.filter { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } } .filter { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
.sortedWith(Comparator<File> { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }) .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
.mapIndexed { i, file -> .mapIndexed { i, file ->
val streamFn = { FileInputStream(file) } val streamFn = { FileInputStream(file) }
ReaderPage(i).apply { ReaderPage(i).apply {
@@ -8,7 +8,6 @@ import eu.kanade.tachiyomi.util.system.ImageUtil
import rx.Observable import rx.Observable
import java.io.File import java.io.File
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.util.zip.ZipEntry
import java.util.zip.ZipFile import java.util.zip.ZipFile
/** /**
@@ -40,7 +39,7 @@ class ZipPageLoader(file: File) : PageLoader() {
override fun getPages(): Observable<List<ReaderPage>> { override fun getPages(): Observable<List<ReaderPage>> {
return zip.entries().toList() return zip.entries().toList()
.filter { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } } .filter { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
.sortedWith(Comparator<ZipEntry> { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }) .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
.mapIndexed { i, entry -> .mapIndexed { i, entry ->
val streamFn = { zip.getInputStream(entry) } val streamFn = { zip.getInputStream(entry) }
ReaderPage(i).apply { ReaderPage(i).apply {
@@ -185,7 +185,7 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
* activity of the change and requests the preload of the next chapter if this is the last page. * activity of the change and requests the preload of the next chapter if this is the last page.
*/ */
private fun onReaderPageSelected(page: ReaderPage, allowPreload: Boolean) { private fun onReaderPageSelected(page: ReaderPage, allowPreload: Boolean) {
val pages = page.chapter.pages!! // Won't be null because it's the loaded chapter val pages = page.chapter.pages ?: return
Timber.d("onReaderPageSelected: ${page.number}/${pages.size}") Timber.d("onReaderPageSelected: ${page.number}/${pages.size}")
activity.onPageSelected(page) activity.onPageSelected(page)
@@ -19,6 +19,10 @@ class WebtoonConfig(preferences: PreferencesHelper = Injekt.get()) : ViewerConfi
// SY --> // SY -->
var enableZoomOut = false var enableZoomOut = false
private set private set
var continuesCropBorders = false
private set
var zoomPropertyChangedListener: ((Boolean) -> Unit)? = null var zoomPropertyChangedListener: ((Boolean) -> Unit)? = null
// SY <-- // SY <--
@@ -32,6 +36,9 @@ class WebtoonConfig(preferences: PreferencesHelper = Injekt.get()) : ViewerConfi
// SY --> // SY -->
preferences.webtoonEnableZoomOut() preferences.webtoonEnableZoomOut()
.register({ enableZoomOut = it }, { zoomPropertyChangedListener?.invoke(it) }) .register({ enableZoomOut = it }, { zoomPropertyChangedListener?.invoke(it) })
preferences.cropBordersContinuesVertical()
.register({ continuesCropBorders = it }, { imagePropertyChangedListener?.invoke() })
// SY <-- // SY <--
} }
} }
@@ -361,7 +361,7 @@ class WebtoonPageHolder(
setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_FIT_WIDTH) setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_FIT_WIDTH)
setMinimumDpi(90) setMinimumDpi(90)
setMinimumTileDpi(180) setMinimumTileDpi(180)
setCropBorders(config.imageCropBorders) setCropBorders(/* SY --> */ if (viewer.isContinuous) config.continuesCropBorders else /* SY <-- */ config.imageCropBorders)
setOnImageEventListener( setOnImageEventListener(
object : SubsamplingScaleImageView.DefaultOnImageEventListener() { object : SubsamplingScaleImageView.DefaultOnImageEventListener() {
override fun onReady() { override fun onReady() {
@@ -191,7 +191,7 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
* activity of the change and requests the preload of the next chapter if this is the last page. * activity of the change and requests the preload of the next chapter if this is the last page.
*/ */
private fun onPageSelected(page: ReaderPage, allowPreload: Boolean) { private fun onPageSelected(page: ReaderPage, allowPreload: Boolean) {
val pages = page.chapter.pages!! // Won't be null because it's the loaded chapter val pages = page.chapter.pages ?: return
Timber.d("onPageSelected: ${page.number}/${pages.size}") Timber.d("onPageSelected: ${page.number}/${pages.size}")
activity.onPageSelected(page) activity.onPageSelected(page)
@@ -320,11 +320,11 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
} }
KeyEvent.KEYCODE_MENU -> if (isUp) activity.toggleMenu() KeyEvent.KEYCODE_MENU -> if (isUp) activity.toggleMenu()
KeyEvent.KEYCODE_DPAD_RIGHT, KeyEvent.KEYCODE_DPAD_LEFT,
KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_DPAD_UP,
KeyEvent.KEYCODE_PAGE_UP -> if (isUp) scrollUp() KeyEvent.KEYCODE_PAGE_UP -> if (isUp) scrollUp()
KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_DPAD_RIGHT,
KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_DPAD_DOWN,
KeyEvent.KEYCODE_PAGE_DOWN -> if (isUp) scrollDown() KeyEvent.KEYCODE_PAGE_DOWN -> if (isUp) scrollDown()
else -> return false else -> return false
@@ -2,26 +2,26 @@ package eu.kanade.tachiyomi.ui.recent
import android.text.format.DateUtils import android.text.format.DateUtils
import android.view.View import android.view.View
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractHeaderItem 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.RecentSectionItemBinding
import java.util.Date import java.util.Date
class DateSectionItem(val date: Date) : AbstractHeaderItem<DateSectionItem.Holder>() { class DateSectionItem(val date: Date) : AbstractHeaderItem<DateSectionItem.DateSectionItemHolder>() {
override fun getLayoutRes(): Int { override fun getLayoutRes(): Int {
return R.layout.recent_section_item return R.layout.recent_section_item
} }
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder { override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): DateSectionItemHolder {
return Holder(view, adapter) return DateSectionItemHolder(view, adapter)
} }
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) { override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: DateSectionItemHolder, position: Int, payloads: List<Any?>?) {
holder.bind(this) holder.bind(this)
} }
@@ -37,14 +37,14 @@ class DateSectionItem(val date: Date) : AbstractHeaderItem<DateSectionItem.Holde
return date.hashCode() return date.hashCode()
} }
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter, true) { inner class DateSectionItemHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter, true) {
private val binding = RecentSectionItemBinding.bind(view)
private val now = Date().time private val now = Date().time
val section_text: TextView = view.findViewById(R.id.section_text)
fun bind(item: DateSectionItem) { fun bind(item: DateSectionItem) {
section_text.text = DateUtils.getRelativeTimeSpanString(item.date.time, now, DateUtils.DAY_IN_MILLIS) binding.sectionText.text = DateUtils.getRelativeTimeSpanString(item.date.time, now, DateUtils.DAY_IN_MILLIS)
} }
} }
} }
@@ -141,7 +141,7 @@ class SettingsAdvancedController : SettingsController() {
switchPreference { switchPreference {
key = Keys.enableDoh key = Keys.enableDoh
titleRes = R.string.pref_dns_over_https titleRes = R.string.pref_dns_over_https
summaryRes = R.string.pref_dns_over_https_summary summaryRes = R.string.requires_app_restart
defaultValue = false defaultValue = false
} }
} }
@@ -39,7 +39,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.system.getFilePicker
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@@ -125,13 +124,11 @@ class SettingsBackupController : SettingsController() {
titleRes = R.string.pref_backup_directory titleRes = R.string.pref_backup_directory
onClick { onClick {
val currentDir = preferences.backupsDirectory().get()
try { try {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
startActivityForResult(intent, CODE_BACKUP_DIR) startActivityForResult(intent, CODE_BACKUP_DIR)
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
// Fall back to custom picker on error activity?.toast(R.string.file_picker_error)
startActivityForResult(preferences.context.getFilePicker(currentDir), CODE_BACKUP_DIR)
} }
} }
@@ -205,7 +202,7 @@ class SettingsBackupController : SettingsController() {
) )
} }
CODE_BACKUP_RESTORE -> { CODE_BACKUP_RESTORE -> {
uri?.path?.let { path -> uri?.path?.let {
val fileName = DocumentFile.fromSingleUri(activity, uri)!!.name!! val fileName = DocumentFile.fromSingleUri(activity, uri)!!.name!!
when { when {
fileName.endsWith(".proto.gz") -> { fileName.endsWith(".proto.gz") -> {
@@ -258,7 +255,6 @@ class SettingsBackupController : SettingsController() {
fun createBackup(flags: Int, type: Int) { fun createBackup(flags: Int, type: Int) {
backupFlags = flags backupFlags = flags
val currentDir = preferences.backupsDirectory().get()
val code = when (type) { val code = when (type) {
BackupConst.BACKUP_TYPE_FULL -> CODE_FULL_BACKUP_CREATE BackupConst.BACKUP_TYPE_FULL -> CODE_FULL_BACKUP_CREATE
else -> CODE_LEGACY_BACKUP_CREATE else -> CODE_LEGACY_BACKUP_CREATE
@@ -277,8 +273,7 @@ class SettingsBackupController : SettingsController() {
startActivityForResult(intent, code) startActivityForResult(intent, code)
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
// Handle errors where the Android ROM doesn't support the built in picker activity?.toast(R.string.file_picker_error)
startActivityForResult(preferences.context.getFilePicker(currentDir), code)
} }
} }
@@ -2,10 +2,13 @@ package eu.kanade.tachiyomi.ui.setting
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.category.repos.RepoController
import eu.kanade.tachiyomi.ui.category.sources.SourceCategoryController import eu.kanade.tachiyomi.ui.category.sources.SourceCategoryController
import eu.kanade.tachiyomi.util.preference.defaultValue import eu.kanade.tachiyomi.util.preference.defaultValue
import eu.kanade.tachiyomi.util.preference.infoPreference
import eu.kanade.tachiyomi.util.preference.onChange import eu.kanade.tachiyomi.util.preference.onChange
import eu.kanade.tachiyomi.util.preference.onClick import eu.kanade.tachiyomi.util.preference.onClick
import eu.kanade.tachiyomi.util.preference.preference import eu.kanade.tachiyomi.util.preference.preference
@@ -13,6 +16,7 @@ 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 kotlinx.coroutines.flow.launchIn
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
class SettingsBrowseController : SettingsController() { class SettingsBrowseController : SettingsController() {
@@ -81,6 +85,19 @@ class SettingsBrowseController : SettingsController() {
true true
} }
} }
// SY -->
preference {
key = "pref_edit_extension_repos"
titleRes = R.string.action_edit_repos
val catCount = preferences.extensionRepos().get().count()
summary = context.resources.getQuantityString(R.plurals.num_repos, catCount, catCount)
onClick {
router.pushController(RepoController().withFadeTransaction())
}
}
// SY <--
} }
preferenceCategory { preferenceCategory {
@@ -93,27 +110,29 @@ class SettingsBrowseController : SettingsController() {
} }
} }
// preferenceCategory { preferenceCategory {
// titleRes = R.string.pref_category_nsfw_content titleRes = R.string.pref_category_nsfw_content
//
// listPreference { switchPreference {
// key = Keys.allowNsfwSource key = Keys.showNsfwSource
// titleRes = R.string.pref_allow_nsfw_sources titleRes = R.string.pref_show_nsfw_source
// entriesRes = arrayOf( summaryRes = R.string.requires_app_restart
// R.string.pref_allow_nsfw_sources_allowed, defaultValue = true
// R.string.pref_allow_nsfw_sources_allowed_multisource, }
// R.string.pref_allow_nsfw_sources_blocked switchPreference {
// ) key = Keys.showNsfwExtension
// entryValues = arrayOf( titleRes = R.string.pref_show_nsfw_extension
// PreferenceValues.NsfwAllowance.ALLOWED.name, defaultValue = true
// PreferenceValues.NsfwAllowance.PARTIAL.name, }
// PreferenceValues.NsfwAllowance.BLOCKED.name switchPreference {
// ) key = Keys.labelNsfwExtension
// defaultValue = PreferenceValues.NsfwAllowance.ALLOWED.name titleRes = R.string.pref_label_nsfw_extension
// summary = "%s" defaultValue = true
// }
// preferences.showNsfwExtension().asImmediateFlow { isVisible = it }.launchIn(scope)
// infoPreference(R.string.parental_controls_info) }
// }
infoPreference(R.string.parental_controls_info)
}
} }
} }
@@ -27,7 +27,7 @@ import eu.kanade.tachiyomi.util.preference.preference
import eu.kanade.tachiyomi.util.preference.preferenceCategory import eu.kanade.tachiyomi.util.preference.preferenceCategory
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.system.getFilePicker import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@@ -203,12 +203,12 @@ class SettingsDownloadController : SettingsController() {
preferences.downloadsDirectory().set(path.toString()) preferences.downloadsDirectory().set(path.toString())
} }
fun customDirectorySelected(currentDir: String) { fun customDirectorySelected() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
try { try {
startActivityForResult(intent, DOWNLOAD_DIR) startActivityForResult(intent, DOWNLOAD_DIR)
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
startActivityForResult(preferences.context.getFilePicker(currentDir), DOWNLOAD_DIR) activity?.toast(R.string.file_picker_error)
} }
} }
@@ -229,7 +229,7 @@ class SettingsDownloadController : SettingsController() {
) { _, position, text -> ) { _, position, text ->
val target = targetController as? SettingsDownloadController val target = targetController as? SettingsDownloadController
if (position == externalDirs.lastIndex) { if (position == externalDirs.lastIndex) {
target?.customDirectorySelected(currentDir) target?.customDirectorySelected()
} else { } else {
target?.predefinedDirectorySelected(text.toString()) target?.predefinedDirectorySelected(text.toString())
} }
@@ -317,6 +317,11 @@ class SettingsReaderController : SettingsController() {
summaryRes = R.string.tap_scroll_page_summary summaryRes = R.string.tap_scroll_page_summary
defaultValue = false defaultValue = false
} }
switchPreference {
key = Keys.cropBordersContinuesVertical
titleRes = R.string.pref_crop_borders
defaultValue = false
}
} }
// SY <-- // SY <--
@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.setting
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import androidx.browser.customtabs.CustomTabsIntent
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
@@ -20,7 +19,7 @@ import eu.kanade.tachiyomi.util.preference.onClick
import eu.kanade.tachiyomi.util.preference.preferenceCategory import eu.kanade.tachiyomi.util.preference.preferenceCategory
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.system.getResourceColor import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.widget.preference.LoginPreference import eu.kanade.tachiyomi.widget.preference.LoginPreference
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,11 +46,9 @@ class SettingsTrackingController :
startActivity(MyAnimeListLoginActivity.newIntent(activity!!)) startActivity(MyAnimeListLoginActivity.newIntent(activity!!))
} }
trackPreference(trackManager.aniList) { trackPreference(trackManager.aniList) {
val tabsIntent = CustomTabsIntent.Builder() activity?.openInBrowser(AnilistApi.authUrl(), trackManager.aniList.getLogoColor()) {
.setToolbarColor(context.getResourceColor(R.attr.colorPrimary)) intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
.build() }
tabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
tabsIntent.launchUrl(activity!!, AnilistApi.authUrl())
} }
trackPreference(trackManager.kitsu) { trackPreference(trackManager.kitsu) {
val dialog = TrackLoginDialog(trackManager.kitsu, R.string.email) val dialog = TrackLoginDialog(trackManager.kitsu, R.string.email)
@@ -59,18 +56,14 @@ class SettingsTrackingController :
dialog.showDialog(router) dialog.showDialog(router)
} }
trackPreference(trackManager.shikimori) { trackPreference(trackManager.shikimori) {
val tabsIntent = CustomTabsIntent.Builder() activity?.openInBrowser(ShikimoriApi.authUrl(), trackManager.shikimori.getLogoColor()) {
.setToolbarColor(context.getResourceColor(R.attr.colorPrimary)) intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
.build() }
tabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
tabsIntent.launchUrl(activity!!, ShikimoriApi.authUrl())
} }
trackPreference(trackManager.bangumi) { trackPreference(trackManager.bangumi) {
val tabsIntent = CustomTabsIntent.Builder() activity?.openInBrowser(BangumiApi.authUrl(), trackManager.bangumi.getLogoColor()) {
.setToolbarColor(context.getResourceColor(R.attr.colorPrimary)) intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
.build() }
tabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
tabsIntent.launchUrl(activity!!, BangumiApi.authUrl())
} }
} }
preferenceCategory { preferenceCategory {
@@ -35,7 +35,7 @@ object SettingsSearchHelper {
* All subclasses of `SettingsController` should be listed here, in order to have their preferences searchable. * All subclasses of `SettingsController` should be listed here, in order to have their preferences searchable.
*/ */
// SY --> // SY -->
private val settingControllersList: List<KClass<out SettingsController>> = { private val settingControllersList: List<KClass<out SettingsController>> = run {
val controllers = mutableListOf( val controllers = mutableListOf(
SettingsAdvancedController::class, SettingsAdvancedController::class,
SettingsBackupController::class, SettingsBackupController::class,
@@ -55,7 +55,7 @@ object SettingsSearchHelper {
controllers += SettingsEhController::class controllers += SettingsEhController::class
} }
controllers controllers
}() }
// SY <-- // SY <--
/** /**
@@ -119,7 +119,7 @@ object SettingsSearchHelper {
(pref.title != null) -> { (pref.title != null) -> {
// Is an actual preference // Is an actual preference
val title = pref.title.toString() val title = pref.title.toString()
val summary = if (pref.summary != null) pref.summary.toString() else "" val summary = pref.summary?.toString() ?: ""
val breadcrumbsStr = addLocalizedBreadcrumb(breadcrumbs, "${pref.title}") val breadcrumbsStr = addLocalizedBreadcrumb(breadcrumbs, "${pref.title}")
prefSearchResultList.add( prefSearchResultList.add(
@@ -1,30 +1,14 @@
package eu.kanade.tachiyomi.ui.setting.track package eu.kanade.tachiyomi.ui.setting.track
import android.content.Intent import android.net.Uri
import android.os.Bundle
import android.view.Gravity.CENTER
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.FrameLayout
import android.widget.ProgressBar
import androidx.appcompat.app.AppCompatActivity
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.ui.main.MainActivity
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy
class AnilistLoginActivity : AppCompatActivity() { class AnilistLoginActivity : BaseOAuthLoginActivity() {
private val trackManager: TrackManager by injectLazy()
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
val view = ProgressBar(this)
setContentView(view, FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, CENTER))
override fun handleResult(data: Uri?) {
val regex = "(?:access_token=)(.*?)(?:&)".toRegex() val regex = "(?:access_token=)(.*?)(?:&)".toRegex()
val matchResult = regex.find(intent.data?.fragment.toString()) val matchResult = regex.find(data?.fragment.toString())
if (matchResult?.groups?.get(1) != null) { if (matchResult?.groups?.get(1) != null) {
trackManager.aniList.login(matchResult.groups[1]!!.value) trackManager.aniList.login(matchResult.groups[1]!!.value)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
@@ -42,12 +26,4 @@ class AnilistLoginActivity : AppCompatActivity() {
returnToSettings() returnToSettings()
} }
} }
private fun returnToSettings() {
finish()
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
startActivity(intent)
}
} }
@@ -1,29 +1,13 @@
package eu.kanade.tachiyomi.ui.setting.track package eu.kanade.tachiyomi.ui.setting.track
import android.content.Intent import android.net.Uri
import android.os.Bundle
import android.view.Gravity.CENTER
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.FrameLayout
import android.widget.ProgressBar
import androidx.appcompat.app.AppCompatActivity
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.ui.main.MainActivity
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy
class BangumiLoginActivity : AppCompatActivity() { class BangumiLoginActivity : BaseOAuthLoginActivity() {
private val trackManager: TrackManager by injectLazy() override fun handleResult(data: Uri?) {
val code = data?.getQueryParameter("code")
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
val view = ProgressBar(this)
setContentView(view, FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, CENTER))
val code = intent.data?.getQueryParameter("code")
if (code != null) { if (code != null) {
trackManager.bangumi.login(code) trackManager.bangumi.login(code)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
@@ -41,12 +25,4 @@ class BangumiLoginActivity : AppCompatActivity() {
returnToSettings() returnToSettings()
} }
} }
private fun returnToSettings() {
finish()
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
startActivity(intent)
}
} }
@@ -0,0 +1,44 @@
package eu.kanade.tachiyomi.ui.setting.track
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.Gravity
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ProgressBar
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.ui.base.activity.BaseThemedActivity
import eu.kanade.tachiyomi.ui.main.MainActivity
import uy.kohesive.injekt.injectLazy
abstract class BaseOAuthLoginActivity : BaseThemedActivity() {
internal val trackManager: TrackManager by injectLazy()
abstract fun handleResult(data: Uri?)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val view = ProgressBar(this)
setContentView(
view,
FrameLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
Gravity.CENTER
)
)
handleResult(intent.data)
}
internal fun returnToSettings() {
finish()
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
startActivity(intent)
}
}
@@ -1,29 +1,13 @@
package eu.kanade.tachiyomi.ui.setting.track package eu.kanade.tachiyomi.ui.setting.track
import android.content.Intent import android.net.Uri
import android.os.Bundle
import android.view.Gravity.CENTER
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.FrameLayout
import android.widget.ProgressBar
import androidx.appcompat.app.AppCompatActivity
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.ui.main.MainActivity
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy
class ShikimoriLoginActivity : AppCompatActivity() { class ShikimoriLoginActivity : BaseOAuthLoginActivity() {
private val trackManager: TrackManager by injectLazy() override fun handleResult(data: Uri?) {
val code = data?.getQueryParameter("code")
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
val view = ProgressBar(this)
setContentView(view, FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, CENTER))
val code = intent.data?.getQueryParameter("code")
if (code != null) { if (code != null) {
trackManager.shikimori.login(code) trackManager.shikimori.login(code)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
@@ -41,12 +25,4 @@ class ShikimoriLoginActivity : AppCompatActivity() {
returnToSettings() returnToSettings()
} }
} }
private fun returnToSettings() {
finish()
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
startActivity(intent)
}
} }
@@ -10,7 +10,7 @@ import androidx.core.view.isVisible
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.WebviewActivityBinding import eu.kanade.tachiyomi.databinding.WebviewActivityBinding
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity import eu.kanade.tachiyomi.ui.base.activity.BaseViewBindingActivity
import eu.kanade.tachiyomi.util.system.WebViewUtil import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.setDefaultSettings import eu.kanade.tachiyomi.util.system.setDefaultSettings
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
@@ -19,7 +19,7 @@ import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.appcompat.navigationClicks import reactivecircus.flowbinding.appcompat.navigationClicks
import reactivecircus.flowbinding.swiperefreshlayout.refreshes import reactivecircus.flowbinding.swiperefreshlayout.refreshes
open class BaseWebViewActivity : BaseActivity<WebviewActivityBinding>() { open class BaseWebViewActivity : BaseViewBindingActivity<WebviewActivityBinding>() {
internal var bundle: Bundle? = null internal var bundle: Bundle? = null
@@ -31,15 +31,17 @@ open class BaseWebViewActivity : BaseActivity<WebviewActivityBinding>() {
if (!WebViewUtil.supportsWebView(this)) { if (!WebViewUtil.supportsWebView(this)) {
toast(R.string.information_webview_required, Toast.LENGTH_LONG) toast(R.string.information_webview_required, Toast.LENGTH_LONG)
finish() finish()
return
} }
try { try {
binding = WebviewActivityBinding.inflate(layoutInflater) binding = WebviewActivityBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
} catch (e: Exception) { } catch (e: Throwable) {
// Potentially throws errors like "Error inflating class android.webkit.WebView" // Potentially throws errors like "Error inflating class android.webkit.WebView"
toast(R.string.information_webview_required, Toast.LENGTH_LONG) toast(R.string.information_webview_required, Toast.LENGTH_LONG)
finish() finish()
return
} }
title = intent.extras?.getString(TITLE_KEY) title = intent.extras?.getString(TITLE_KEY)
@@ -181,7 +181,7 @@ private suspend fun <T> Observable<T>.awaitOne(): T = suspendCancellableCoroutin
internal fun <T> CancellableContinuation<T>.unsubscribeOnCancellation(sub: Subscription) = internal fun <T> CancellableContinuation<T>.unsubscribeOnCancellation(sub: Subscription) =
invokeOnCancellation { sub.unsubscribe() } invokeOnCancellation { sub.unsubscribe() }
fun <T : Any> Observable<T>.asFlow(): Flow<T> = callbackFlow { fun <T : Any?> Observable<T>.asFlow(): Flow<T> = callbackFlow {
val observer = object : Observer<T> { val observer = object : Observer<T> {
override fun onNext(t: T) { override fun onNext(t: T) {
offer(t) offer(t)
@@ -199,7 +199,7 @@ fun <T : Any> Observable<T>.asFlow(): Flow<T> = callbackFlow {
awaitClose { subscription.unsubscribe() } awaitClose { subscription.unsubscribe() }
} }
fun <T : Any> Flow<T>.asObservable(backpressureMode: Emitter.BackpressureMode = Emitter.BackpressureMode.NONE): Observable<T> { fun <T : Any?> Flow<T>.asObservable(backpressureMode: Emitter.BackpressureMode = Emitter.BackpressureMode.NONE): Observable<T> {
return Observable.create( return Observable.create(
{ emitter -> { emitter ->
/* /*
@@ -13,12 +13,14 @@ import android.content.pm.PackageManager
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Color import android.graphics.Color
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.Uri
import android.os.PowerManager import android.os.PowerManager
import android.view.View import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@@ -29,10 +31,8 @@ import androidx.core.graphics.green
import androidx.core.graphics.red import androidx.core.graphics.red
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.nononsenseapps.filepicker.FilePickerActivity
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 eu.kanade.tachiyomi.widget.CustomLayoutPickerActivity
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -109,19 +109,6 @@ fun Context.notification(channelId: String, block: (NotificationCompat.Builder.(
return builder.build() return builder.build()
} }
/**
* Helper method to construct an Intent to use a custom file picker.
* @param currentDir the path the file picker will open with.
* @return an Intent to start the file picker activity.
*/
fun Context.getFilePicker(currentDir: String): Intent {
return Intent(this, CustomLayoutPickerActivity::class.java)
.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false)
.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true)
.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR)
.putExtra(FilePickerActivity.EXTRA_START_PATH, currentDir)
}
/** /**
* Checks if the give permission is granted. * Checks if the give permission is granted.
* *
@@ -250,12 +237,21 @@ fun Context.isServiceRunning(serviceClass: Class<*>): Boolean {
/** /**
* Opens a URL in a custom tab. * Opens a URL in a custom tab.
*/ */
fun Context.openInBrowser(url: String) { fun Context.openInBrowser(url: String, @ColorInt toolbarColor: Int? = null, block: CustomTabsIntent.() -> Unit = {}) {
this.openInBrowser(url.toUri(), toolbarColor, block)
}
fun Context.openInBrowser(uri: Uri, @ColorInt toolbarColor: Int? = null, block: CustomTabsIntent.() -> Unit = {}) {
try { try {
val intent = CustomTabsIntent.Builder() val intent = CustomTabsIntent.Builder()
.setToolbarColor(getResourceColor(R.attr.colorPrimary)) .setDefaultColorSchemeParams(
CustomTabColorSchemeParams.Builder()
.setToolbarColor(toolbarColor ?: getResourceColor(R.attr.colorPrimary))
.build()
)
.build() .build()
intent.launchUrl(this, url.toUri()) block(intent)
intent.launchUrl(this, uri)
} catch (e: Exception) { } catch (e: Exception) {
toast(e.message) toast(e.message)
} }
@@ -13,7 +13,7 @@ object WebViewUtil {
const val REQUESTED_WITH = "com.android.browser" const val REQUESTED_WITH = "com.android.browser"
const val MINIMUM_WEBVIEW_VERSION = 84 const val MINIMUM_WEBVIEW_VERSION = 86
fun supportsWebView(context: Context): Boolean { fun supportsWebView(context: Context): Boolean {
try { try {
@@ -1,33 +0,0 @@
package eu.kanade.tachiyomi.widget
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.nononsenseapps.filepicker.AbstractFilePickerFragment
import com.nononsenseapps.filepicker.FilePickerActivity
import com.nononsenseapps.filepicker.FilePickerFragment
import com.nononsenseapps.filepicker.LogicHandler
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.view.inflate
import java.io.File
class CustomLayoutPickerActivity : FilePickerActivity() {
override fun getFragment(startPath: String?, mode: Int, allowMultiple: Boolean, allowCreateDir: Boolean):
AbstractFilePickerFragment<File> {
val fragment = CustomLayoutFilePickerFragment()
fragment.setArgs(startPath, mode, allowMultiple, allowCreateDir)
return fragment
}
}
class CustomLayoutFilePickerFragment : FilePickerFragment() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
LogicHandler.VIEWTYPE_DIR -> {
val view = parent.inflate(R.layout.common_listitem_dir)
DirViewHolder(view)
}
else -> super.onCreateViewHolder(parent, viewType)
}
}
}
+10 -2
View File
@@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.source.online.all.EHentai import eu.kanade.tachiyomi.source.online.all.EHentai
@@ -14,6 +15,8 @@ import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.lang.awaitSingle import eu.kanade.tachiyomi.util.lang.awaitSingle
import exh.source.getMainSource import exh.source.getMainSource
import exh.util.executeOnIO import exh.util.executeOnIO
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
class GalleryAdder { class GalleryAdder {
@@ -22,6 +25,11 @@ class GalleryAdder {
private val sourceManager: SourceManager by injectLazy() private val sourceManager: SourceManager by injectLazy()
private val filters: Pair<Set<String>, Set<Long>> = run {
val preferences = Injekt.get<PreferencesHelper>()
preferences.enabledLanguages().get() to preferences.disabledSources().get().map { it.toLong() }.toSet()
}
private val logger = XLog.tag("GalleryAdder").enableStackTrace(2).build() private val logger = XLog.tag("GalleryAdder").enableStackTrace(2).build()
fun pickSource(url: String): List<UrlImportableSource> { fun pickSource(url: String): List<UrlImportableSource> {
@@ -30,7 +38,7 @@ class GalleryAdder {
.map { it.getMainSource() } .map { it.getMainSource() }
.filterIsInstance<UrlImportableSource>() .filterIsInstance<UrlImportableSource>()
.filter { .filter {
try { it.lang in filters.first && it.id !in filters.second && try {
it.matchesUri(uri) it.matchesUri(uri)
} catch (e: Exception) { } catch (e: Exception) {
false false
@@ -63,7 +71,7 @@ class GalleryAdder {
.map { it.getMainSource() } .map { it.getMainSource() }
.filterIsInstance<UrlImportableSource>() .filterIsInstance<UrlImportableSource>()
.find { .find {
try { it.lang in filters.first && it.id !in filters.second && try {
it.matchesUri(uri) it.matchesUri(uri)
} catch (e: Exception) { } catch (e: Exception) {
false false
+10 -11
View File
@@ -35,18 +35,17 @@ object MetadataUtil {
private const val GB_FACTOR = 1000 * MB_FACTOR private const val GB_FACTOR = 1000 * MB_FACTOR
private const val GIB_FACTOR = 1024 * MIB_FACTOR private const val GIB_FACTOR = 1024 * MIB_FACTOR
fun parseHumanReadableByteCount(arg0: String): Double? { fun parseHumanReadableByteCount(bytes: String): Double? {
val spaceNdx = arg0.indexOf(" ") val ret = bytes.substringBefore(' ').toDouble()
val ret = java.lang.Double.parseDouble(arg0.substring(0, spaceNdx)) return when (bytes.substringAfter(' ')) {
when (arg0.substring(spaceNdx + 1)) { "GB" -> ret * GB_FACTOR
"GB" -> return ret * GB_FACTOR "GiB" -> ret * GIB_FACTOR
"GiB" -> return ret * GIB_FACTOR "MB" -> ret * MB_FACTOR
"MB" -> return ret * MB_FACTOR "MiB" -> ret * MIB_FACTOR
"MiB" -> return ret * MIB_FACTOR "KB" -> ret * KB_FACTOR
"KB" -> return ret * KB_FACTOR "KiB" -> ret * KIB_FACTOR
"KiB" -> return ret * KIB_FACTOR else -> null
} }
return null
} }
val ONGOING_SUFFIX = arrayOf( val ONGOING_SUFFIX = arrayOf(
@@ -59,8 +59,8 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
titleObj?.let { manga.title = it } titleObj?.let { manga.title = it }
// Set artist (if we can find one) // Set artist (if we can find one)
tags.filter { it.namespace == EH_ARTIST_NAMESPACE }.let { tags.filter { it.namespace == EH_ARTIST_NAMESPACE }.let { tags ->
if (it.isNotEmpty()) manga.artist = it.joinToString(transform = { it.name }) if (tags.isNotEmpty()) manga.artist = tags.joinToString(transform = { it.name })
} }
// Copy tags -> genres // Copy tags -> genres
@@ -77,35 +77,7 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
} }
} }
// Build a nice looking description out of what we know manga.description = "meta"
/* val titleDesc = StringBuilder()
title?.let { titleDesc += "Title: $it\n" }
altTitle?.let { titleDesc += "Alternate Title: $it\n" }
val detailsDesc = StringBuilder()
genre?.let { detailsDesc += "Genre: $it\n" }
uploader?.let { detailsDesc += "Uploader: $it\n" }
datePosted?.let { detailsDesc += "Posted: ${EX_DATE_FORMAT.format(Date(it))}\n" }
visible?.let { detailsDesc += "Visible: $it\n" }
language?.let {
detailsDesc += "Language: $it"
if (translated == true) detailsDesc += " TR"
detailsDesc += "\n"
}
size?.let { detailsDesc += "File size: ${humanReadableByteCount(it, true)}\n" }
length?.let { detailsDesc += "Length: $it pages\n" }
favorites?.let { detailsDesc += "Favorited: $it times\n" }
averageRating?.let {
detailsDesc += "Rating: $it"
ratingCount?.let { detailsDesc += " ($it)" }
detailsDesc += "\n"
}
val tagsDesc = tagsToDescription()*/
manga.description = "meta" /*listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString())
.filter(String::isNotBlank)
.joinToString(separator = "\n")*/
} }
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> { override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
@@ -29,14 +29,7 @@ class EightMusesSearchMetadata : RaisedSearchMetadata() {
manga.genre = tagsToGenreString() manga.genre = tagsToGenreString()
/*val titleDesc = StringBuilder() manga.description = "meta"
title?.let { titleDesc += "Title: $it\n" }
val tagsDesc = tagsToDescription()*/
manga.description = "meta" /*listOf(titleDesc.toString(), tagsDesc.toString())
.filter(String::isNotBlank)
.joinToString(separator = "\n")*/
} }
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> { override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
@@ -37,15 +37,7 @@ class HBrowseSearchMetadata : RaisedSearchMetadata() {
manga.genre = tagsToGenreString() manga.genre = tagsToGenreString()
/*val titleDesc = StringBuilder() manga.description = "meta"
title?.let { titleDesc += "Title: $it\n" }
length?.let { titleDesc += "Length: $it page(s)\n" }
val tagsDesc = tagsToDescription()*/
manga.description = "meta" /*listOf(titleDesc.toString(), tagsDesc.toString())
.filter(String::isNotBlank)
.joinToString(separator = "\n")*/
} }
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> { override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
@@ -36,14 +36,7 @@ class HentaiCafeSearchMetadata : RaisedSearchMetadata() {
manga.genre = tagsToGenreString() manga.genre = tagsToGenreString()
/* val detailsDesc = "Title: $title\n" + manga.description = "meta"
"Artist: $artist\n"
val tagsDesc = tagsToDescription()*/
manga.description = "meta" /*listOf(detailsDesc, tagsDesc.toString())
.filter(String::isNotBlank)
.joinToString(separator = "\n")*/
} }
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> { override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
@@ -51,45 +51,7 @@ class HitomiSearchMetadata : RaisedSearchMetadata() {
manga.status = SManga.UNKNOWN manga.status = SManga.UNKNOWN
/*val titleDesc = StringBuilder() manga.description = "meta"
title?.let {
titleDesc += "Title: $it\n"
}
val detailsDesc = StringBuilder()
detailsDesc += "Artist(s): ${manga.artist}\n"
group?.let {
detailsDesc += "Group: $it\n"
}
type?.let {
detailsDesc += "Type: ${it.capitalize()}\n"
}
(language ?: "unknown").let {
detailsDesc += "Language: ${it.capitalize()}\n"
}
if (series.isNotEmpty()) {
detailsDesc += "Series: ${series.joinToString()}\n"
}
if (characters.isNotEmpty()) {
detailsDesc += "Characters: ${characters.joinToString()}\n"
}
uploadDate?.let {
detailsDesc += "Upload date: ${EX_DATE_FORMAT.format(Date(it))}\n"
}
val tagsDesc = tagsToDescription()*/
manga.description = "meta" /*listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString())
.filter(String::isNotBlank)
.joinToString(separator = "\n")*/
} }
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> { override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
@@ -53,8 +53,8 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() {
} }
// Set artist (if we can find one) // Set artist (if we can find one)
tags.filter { it.namespace == NHENTAI_ARTIST_NAMESPACE }.let { tags.filter { it.namespace == NHENTAI_ARTIST_NAMESPACE }.let { tags ->
if (it.isNotEmpty()) manga.artist = it.joinToString(transform = { it.name }) if (tags.isNotEmpty()) manga.artist = tags.joinToString(transform = { it.name })
} }
// Copy tags -> genres // Copy tags -> genres
@@ -56,41 +56,7 @@ class PervEdenSearchMetadata : RaisedSearchMetadata() {
// Copy tags -> genres // Copy tags -> genres
manga.genre = tagsToGenreString() manga.genre = tagsToGenreString()
/*val titleDesc = StringBuilder() manga.description = "meta"
title?.let {
titleDesc += "Title: $it\n"
}
if (altTitles.isNotEmpty()) {
titleDesc += "Alternate Titles: \n" + altTitles
.joinToString(separator = "\n", postfix = "\n") {
"$it"
}
}
val detailsDesc = StringBuilder()
artist?.let {
detailsDesc += "Artist: $it\n"
}
type?.let {
detailsDesc += "Type: $it\n"
}
status?.let {
detailsDesc += "Status: $it\n"
}
rating?.let {
detailsDesc += "Rating: %.2\n".format(it)
}
val tagsDesc = tagsToDescription()*/
manga.description = "meta" /*listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString())
.filter(String::isNotBlank)
.joinToString(separator = "\n")*/
} }
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> { override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
@@ -45,21 +45,7 @@ class PururinSearchMetadata : RaisedSearchMetadata() {
manga.genre = tagsToGenreString() manga.genre = tagsToGenreString()
/*val titleDesc = StringBuilder() manga.description = "meta"
title?.let { titleDesc += "English Title: $it\n" }
altTitle?.let { titleDesc += "Japanese Title: $it\n" }
val detailsDesc = StringBuilder()
(uploaderDisp ?: uploader)?.let { detailsDesc += "Uploader: $it\n" }
pages?.let { detailsDesc += "Length: $it pages\n" }
fileSize?.let { detailsDesc += "Size: $it\n" }
ratingCount?.let { detailsDesc += "Rating: $averageRating ($ratingCount)\n" }
val tagsDesc = tagsToDescription()*/
manga.description = "meta" /*listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString())
.filter(String::isNotBlank)
.joinToString(separator = "\n")*/
} }
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> { override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
@@ -52,32 +52,7 @@ class TsuminoSearchMetadata : RaisedSearchMetadata() {
// Copy tags -> genres // Copy tags -> genres
manga.genre = tagsToGenreString() manga.genre = tagsToGenreString()
/*val titleDesc = "Title: $title\n" manga.description = "meta"
val detailsDesc = StringBuilder()
uploader?.let { detailsDesc += "Uploader: $it\n" }
uploadDate?.let { detailsDesc += "Uploaded: ${EX_DATE_FORMAT.format(Date(it))}\n" }
length?.let { detailsDesc += "Length: $it pages\n" }
ratingString?.let { detailsDesc += "Rating: $it\n" }
category?.let {
detailsDesc += "Category: $it\n"
}
collection?.let { detailsDesc += "Collection: $it\n" }
group?.let { detailsDesc += "Group: $it\n" }
val parodiesString = parody.joinToString()
if (parodiesString.isNotEmpty()) {
detailsDesc += "Parody: $parodiesString\n"
}
val charactersString = character.joinToString()
if (charactersString.isNotEmpty()) {
detailsDesc += "Character: $charactersString\n"
}
val tagsDesc = tagsToDescription()*/
manga.description = "meta" /*listOf(titleDesc, detailsDesc.toString(), tagsDesc.toString())
.filter(String::isNotBlank)
.joinToString(separator = "\n")*/
} }
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> { override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
+19 -5
View File
@@ -22,6 +22,7 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
@@ -103,10 +104,21 @@ class Anilist : API("https://graphql.anilist.co/") {
} }
private fun getTitle(obj: JsonObject): String { private fun getTitle(obj: JsonObject): String {
return obj["title"]!!.jsonObject.let { val titleObj = obj["title"]!!.jsonObject
it["romaji"]?.jsonPrimitive?.content
?: it["english"]?.jsonPrimitive?.content val english = titleObj["english"]?.jsonPrimitive?.contentOrNull
?: it["native"]!!.jsonPrimitive.content val romaji = titleObj["romaji"]?.jsonPrimitive?.contentOrNull
val native = titleObj["native"]?.jsonPrimitive?.contentOrNull
val synonym = obj["synonyms"]!!.jsonArray.getOrNull(0)?.jsonPrimitive?.contentOrNull
val isJP = obj["countryOfOrigin"]!!.jsonPrimitive.content == "JP"
return when {
!english.isNullOrBlank() -> english
isJP && !romaji.isNullOrBlank() -> romaji
!synonym.isNullOrBlank() -> synonym
!isJP && !romaji.isNullOrBlank() -> romaji
else -> native ?: "NO NAME FOUND"
} }
} }
@@ -126,12 +138,14 @@ class Anilist : API("https://graphql.anilist.co/") {
|edges { |edges {
|node { |node {
|mediaRecommendation { |mediaRecommendation {
|countryOfOrigin
|siteUrl |siteUrl
|title { |title {
|romaji |romaji
|english |english
|native |native
|} |}
|synonyms
|coverImage { |coverImage {
|large |large
|} |}
@@ -203,7 +217,7 @@ open class RecommendsPager(
recs recs
} catch (e: Exception) { } catch (e: Exception) {
Timber.tag("RECOMMENDATIONS").e("%s > Error: %s", key, e.message) Timber.tag("RECOMMENDATIONS").e("%s > Error: %s", key, e.message)
listOf() listOf<SMangaImpl>()
} }
} }
.firstOrNull { it.isNotEmpty() } .firstOrNull { it.isNotEmpty() }
@@ -0,0 +1,41 @@
package exh.ui.base
import androidx.annotation.CallSuper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import nucleus.presenter.Presenter
@Suppress("DEPRECATION")
open class CoroutinePresenter<V> : Presenter<V>() {
val scope = CoroutineScope(Job() + Dispatchers.Main)
@Suppress("DeprecatedCallableAddReplaceWith")
@Deprecated("Use launchInView")
override fun getView(): V? {
return super.getView()
}
fun launchInView(block: (CoroutineScope, V) -> Unit) = scope.launch(Dispatchers.Main) {
view?.let { block.invoke(this, it) }
}
fun <F> Flow<F>.onEachView(block: (V, F) -> Unit) = onEach {
view?.let { view -> block(view, it) }
}
fun <F, P> Flow<F>.mapView(block: (V, F) -> P): Flow<P> = mapNotNull {
view?.let { view -> block(view, it) }
}
@CallSuper
override fun destroy() {
super.destroy()
scope.cancel()
}
}
@@ -11,7 +11,7 @@ import com.afollestad.materialdialogs.list.listItemsSingleChoice
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.EhActivityInterceptBinding import eu.kanade.tachiyomi.databinding.EhActivityInterceptBinding
import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity import eu.kanade.tachiyomi.ui.base.activity.BaseViewBindingActivity
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
import exh.GalleryAddEvent import exh.GalleryAddEvent
@@ -23,7 +23,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class InterceptActivity : BaseActivity<EhActivityInterceptBinding>() { class InterceptActivity : BaseViewBindingActivity<EhActivityInterceptBinding>() {
private var statusJob: Job? = null private var statusJob: Job? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -11,12 +11,9 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.databinding.MetadataViewControllerBinding import eu.kanade.tachiyomi.databinding.MetadataViewControllerBinding
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.MetadataSource
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import exh.metadata.metadata.base.FlatMetadata
import exh.metadata.metadata.base.RaisedSearchMetadata import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.source.getMainSource
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@@ -74,13 +71,6 @@ class MetadataViewController : NucleusController<MetadataViewControllerBinding,
binding.recycler.setHasFixedSize(true) binding.recycler.setHasFixedSize(true)
} }
fun onNextMetaInfo(flatMetadata: FlatMetadata) {
val mainSource = presenter.source.getMainSource()
if (mainSource is MetadataSource<*, *>) {
presenter.meta = flatMetadata.raise(mainSource.metaClass)
}
}
fun onNextMangaInfo(meta: RaisedSearchMetadata?) { fun onNextMangaInfo(meta: RaisedSearchMetadata?) {
val context = view?.context ?: return val context = view?.context ?: return
data = meta?.getExtraInfoPairs(context).orEmpty() data = meta?.getExtraInfoPairs(context).orEmpty()
@@ -1,17 +1,23 @@
package exh.ui.metadata package exh.ui.metadata
import android.os.Bundle import android.os.Bundle
import com.elvishew.xlog.XLog
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.source.online.MetadataSource
import eu.kanade.tachiyomi.util.lang.asFlow
import exh.metadata.metadata.base.FlatMetadata import exh.metadata.metadata.base.FlatMetadata
import exh.metadata.metadata.base.RaisedSearchMetadata import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.metadata.metadata.base.getFlatMetadataForManga import exh.metadata.metadata.base.getFlatMetadataForManga
import rx.Observable import exh.source.getMainSource
import rx.android.schedulers.AndroidSchedulers import exh.ui.base.CoroutinePresenter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.plus
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@@ -20,29 +26,31 @@ class MetadataViewPresenter(
val source: Source, val source: Source,
val preferences: PreferencesHelper = Injekt.get(), val preferences: PreferencesHelper = Injekt.get(),
private val db: DatabaseHelper = Injekt.get() private val db: DatabaseHelper = Injekt.get()
) : BasePresenter<MetadataViewController>() { ) : CoroutinePresenter<MetadataViewController>() {
var meta: RaisedSearchMetadata? = null val meta = MutableStateFlow<RaisedSearchMetadata?>(null)
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
getMangaMetaObservable().subscribeLatestCache({ view, flatMetadata -> if (flatMetadata != null) view.onNextMetaInfo(flatMetadata) else XLog.tag("MetadataViewPresenter").disableStackTrace().d("Invalid metadata") }) getMangaMetaObservable()
.onEach {
if (it == null) return@onEach
val mainSource = source.getMainSource()
if (mainSource is MetadataSource<*, *>) {
meta.value = it.raise(mainSource.metaClass)
}
}
.launchIn(scope + Dispatchers.IO)
getMangaObservable() meta
.observeOn(AndroidSchedulers.mainThread()) .onEachView { view, metadata ->
.subscribeLatestCache({ view, _ -> view.onNextMangaInfo(meta) }) view.onNextMangaInfo(metadata)
}
.launchIn(scope)
} }
private fun getMangaObservable(): Observable<Manga> { private fun getMangaMetaObservable(): Flow<FlatMetadata?> {
return db.getManga(manga.url, manga.source).asRxObservable() return db.getFlatMetadataForManga(manga.id!!).asRxObservable().asFlow()
}
private fun getMangaMetaObservable(): Observable<FlatMetadata?> {
val mangaId = manga.id
return if (mangaId != null) {
db.getFlatMetadataForManga(mangaId).asRxObservable()
.observeOn(AndroidSchedulers.mainThread())
} else Observable.just(null)
} }
} }
@@ -3,7 +3,6 @@ package exh.widget.preference
import android.app.Dialog import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
@@ -40,7 +39,7 @@ class MangadexLoginDialog(bundle: Bundle? = null) : DialogController(bundle) {
val scope = CoroutineScope(Job() + Dispatchers.Main) val scope = CoroutineScope(Job() + Dispatchers.Main)
var binding: PrefSiteLoginTwoFactorAuthBinding? = null lateinit var binding: PrefSiteLoginTwoFactorAuthBinding
constructor(source: MangaDex) : this( constructor(source: MangaDex) : this(
bundleOf( bundleOf(
@@ -51,31 +50,31 @@ class MangadexLoginDialog(bundle: Bundle? = null) : DialogController(bundle) {
override fun onCreateDialog(savedViewState: Bundle?): Dialog { override fun onCreateDialog(savedViewState: Bundle?): Dialog {
binding = PrefSiteLoginTwoFactorAuthBinding.inflate(LayoutInflater.from(activity!!)) binding = PrefSiteLoginTwoFactorAuthBinding.inflate(LayoutInflater.from(activity!!))
val dialog = MaterialDialog(activity!!) val dialog = MaterialDialog(activity!!)
.customView(view = binding!!.root, scrollable = false) .customView(view = binding.root, scrollable = false)
onViewCreated(dialog.view) onViewCreated()
return dialog return dialog
} }
fun onViewCreated(view: View) { fun onViewCreated() {
binding!!.login.setMode(ActionProcessButton.Mode.ENDLESS) binding.login.setMode(ActionProcessButton.Mode.ENDLESS)
binding!!.login.setOnClickListener { checkLogin() } binding.login.setOnClickListener { checkLogin() }
setCredentialsOnView() setCredentialsOnView()
binding!!.twoFactorCheck.setOnCheckedChangeListener { _, isChecked -> binding.twoFactorCheck.setOnCheckedChangeListener { _, isChecked ->
binding!!.twoFactorHolder.isVisible = isChecked binding.twoFactorHolder.isVisible = isChecked
} }
} }
private fun setCredentialsOnView() { private fun setCredentialsOnView() {
binding?.username?.setText(service.getUsername()) binding.username.setText(service.getUsername())
binding?.password?.setText(service.getPassword()) binding.password.setText(service.getPassword())
} }
private fun checkLogin() { private fun checkLogin() {
binding?.apply { with(binding) {
if (username.text.isNullOrBlank() || password.text.isNullOrBlank() || (twoFactorCheck.isChecked && twoFactorEdit.text.isNullOrBlank())) { if (username.text.isNullOrBlank() || password.text.isNullOrBlank() || (twoFactorCheck.isChecked && twoFactorEdit.text.isNullOrBlank())) {
errorResult() errorResult()
root.context.toast(R.string.fields_cannot_be_blank) root.context.toast(R.string.fields_cannot_be_blank)
@@ -110,7 +109,7 @@ class MangadexLoginDialog(bundle: Bundle? = null) : DialogController(bundle) {
} }
private fun errorResult() { private fun errorResult() {
binding?.apply { with(binding) {
dialog?.setCancelable(true) dialog?.setCancelable(true)
dialog?.setCanceledOnTouchOutside(true) dialog?.setCanceledOnTouchOutside(true)
login.progress = -1 login.progress = -1
@@ -127,7 +126,6 @@ class MangadexLoginDialog(bundle: Bundle? = null) : DialogController(bundle) {
private fun onDialogClosed() { private fun onDialogClosed() {
scope.cancel() scope.cancel()
binding = null
if (activity != null) { if (activity != null) {
(activity as? Listener)?.siteLoginDialogClosed(source!!) (activity as? Listener)?.siteLoginDialogClosed(source!!)
} else { } else {
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M10,4H4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V8c0,-1.1 -0.9,-2 -2,-2h-8l-2,-2z" />
</vector>

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