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)
3. What is your type of issue?
* [Catalogue request](#catalogue-requests)
* [Bugs](#bugs)
* [Feature requests](#feature-requests)
* [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 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
* Include version (More > About > Version)
@@ -23,9 +23,9 @@
* For large logs use http://pastebin.com/ (or similar)
* 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
+2 -2
View File
@@ -2,9 +2,9 @@
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
- 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**
+2 -2
View File
@@ -9,9 +9,9 @@ labels: "bug"
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
- 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**
+1 -1
View File
@@ -4,5 +4,5 @@ contact_links:
url: https://tachiyomi.org/help/
about: Common questions are answered here.
- 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.
+2 -2
View File
@@ -9,9 +9,9 @@ labels: "feature"
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
- 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**
+3 -3
View File
@@ -1,8 +1,8 @@
---
name: "Extension/source/catalogue issue"
about: "Do not open an issue here. See https://github.com/inorichi/tachiyomi-extensions"
title: "THIS ISSUE IS IN THE WRONG REPO; 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/tachiyomiorg/tachiyomi-extensions"
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
build:
name: Build app release
name: Build app
needs: check_wrapper
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
runs-on: ubuntu-latest
@@ -60,21 +60,16 @@ jobs:
dependencies-cache-enabled: true
configuration-cache-enabled: true
- name: Sign Android Release
- name: Sign APK
uses: r0adkll/sign-android-release@v1
with:
# The directory to find your release to sign
releaseDirectory: app/build/outputs/apk/standard/release
# The key used to sign your release in base64 encoded format
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
# The key alias
alias: ${{ secrets.ALIAS }}
# The password to the keystore
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
# The password for the key
keyPassword: ${{ secrets.KEY_PASSWORD }}
- name: Create Release
- name: Create release
id: create_release
uses: actions/create-release@v1
env:
@@ -85,7 +80,7 @@ jobs:
draft: true
prerelease: false
- name: Upload Release APK
- name: Upload APK to release
uses: actions/upload-release-asset@v1
env:
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 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
* 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
@@ -52,18 +52,19 @@ Features of TachiyomiSY include:
* Enhanced views for internal and integrated sources
* Enhanced usability for internal and delegated sources
* Custom sources:
* * 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)
* * HBrowse
* * HentaiCafe (inside Foolside)
* * Hitomi.la
* * Mangadex
* * NHentai
* * PervEden (EN and IT)
* * Puruin
* * Tsumino
Custom sources:
* 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)
* HBrowse
* HentaiCafe (inside Foolside)
* Hitomi.la
* Mangadex
* NHentai
* PervEden (EN and IT)
* Puruin
* Tsumino
## Download
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)
* 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>
@@ -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"
* 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>
## FAQ
+4 -9
View File
@@ -5,8 +5,8 @@ import java.text.SimpleDateFormat
apply plugin: 'com.android.application'
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-parcelize'
apply plugin: 'kotlinx-serialization'
apply plugin: 'com.github.zellius.shortcut-helper'
// Realm (EH)
@@ -44,8 +44,8 @@ android {
minSdkVersion AndroidConfig.minSdk
targetSdkVersion AndroidConfig.targetSdk
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
versionCode 10
versionName "1.4.0"
versionCode 11
versionName "1.4.1"
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
@@ -133,10 +133,6 @@ android {
}
}
androidExtensions {
experimental = true
}
dependencies {
// 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 'eu.davidea:flexible-adapter:5.1.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.github.chrisbanes:PhotoView:2.3.0'
implementation 'com.github.carlosesco:DirectionalViewPager:a844dbca0a'
@@ -294,7 +289,7 @@ dependencies {
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-android:$coroutines_version"
-4
View File
@@ -80,10 +80,6 @@
<activity
android:name=".ui.webview.WebViewActivity"
android:configChanges="uiMode|orientation|screenSize" />
<activity
android:name=".widget.CustomLayoutPickerActivity"
android:label="@string/app_name"
android:theme="@style/FilePickerTheme" />
<activity
android:name=".ui.setting.track.AnilistLoginActivity"
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.extension.ExtensionUpdateJob
import eu.kanade.tachiyomi.ui.library.LibrarySort
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@@ -120,7 +121,10 @@ object Migrations {
// Force MAL log out due to login flow change
val trackManager = Injekt.get<TrackManager>()
trackManager.myAnimeList.logout()
if (trackManager.myAnimeList.isLogged) {
trackManager.myAnimeList.logout()
context.toast(R.string.myanimelist_relogin)
}
}
return true
}
@@ -146,7 +146,7 @@ class BackupNotifier(private val context: Context) {
val uri = destFile.getUriCompat(context)
addAction(
R.drawable.nnf_ic_file_folder,
R.drawable.ic_folder_24dp,
context.getString(R.string.action_open_log),
NotificationReceiver.openErrorLogPendingActivity(context, uri)
)
@@ -137,7 +137,7 @@ class DownloadPendingDeleter(context: Context) {
val id: Long,
val url: String,
val name: String,
val scanlator: String?
val scanlator: String? = null
)
/**
@@ -110,7 +110,7 @@ class LibraryUpdateNotifier(private val context: Context) {
setContentIntent(errorLogIntent)
addAction(
R.drawable.nnf_ic_file_folder,
R.drawable.ic_folder_24dp,
context.getString(R.string.action_open_log),
errorLogIntent
)
@@ -39,6 +39,7 @@ import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.isServiceRunning
import exh.LIBRARY_UPDATE_EXCLUDED_SOURCES
import exh.MERGED_SOURCE_ID
import exh.mangaDexSourceIds
import exh.md.utils.FollowStatus
import exh.md.utils.MdUtil
import exh.metadata.metadata.base.insertFlatMetadata
@@ -283,7 +284,7 @@ class LibraryUpdateService(
val trackingExtra = intent.getStringExtra(KEY_GROUP_EXTRA)?.toIntOrNull() ?: -1
libraryManga.filter {
val loggedServices = trackManager.services.filter { it.isLogged }
val status: String = {
val status: String = run {
val tracks = db.getTracks(it).executeAsBlocking()
val track = tracks.find { track ->
loggedServices.any { it.id == track?.sync_id }
@@ -294,7 +295,7 @@ class LibraryUpdateService(
} else {
"not tracked"
}
}()
}
trackManager.mapTrackingOrder(status, applicationContext) == trackingExtra
}
}
@@ -537,7 +538,9 @@ class LibraryUpdateService(
}
// 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> {
val count = AtomicInteger(0)
val mangaDex = MdUtil.getEnabledMangaDex(preferences, sourceManager) ?: return Observable.empty()
@@ -582,7 +585,7 @@ class LibraryUpdateService(
*/
private fun pushFavorites(): Observable<LibraryManga> {
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
return Observable.from(if (trackManager.mdList.isLogged) listManga else emptyList())
@@ -127,7 +127,9 @@ object PreferenceKeys {
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"
@@ -330,4 +332,8 @@ object PreferenceKeys {
const val createLegacyBackup = "create_legacy_backup"
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
}
enum class NsfwAllowance {
ALLOWED,
PARTIAL,
BLOCKED
}
// SY -->
enum class GroupLibraryMode {
GLOBAL,
@@ -10,7 +10,6 @@ import com.tfcporciuncula.flow.Preference
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
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.anilist.Anilist
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
@@ -231,7 +230,9 @@ class PreferencesHelper(val context: Context) {
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)
@@ -444,4 +445,8 @@ class PreferencesHelper(val context: Context) {
fun sortTagsForLibrary() = flowPrefs.getStringSet(Keys.sortTagsForLibrary, mutableSetOf())
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 eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
@@ -18,9 +19,8 @@ import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonObject
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import rx.Observable
import uy.kohesive.injekt.injectLazy
@@ -30,7 +30,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
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()
fun addLibManga(track: Track): Observable<Track> {
@@ -51,22 +51,18 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("status", track.toAnilistStatus())
}
}
val body = payload.toString().toRequestBody(jsonMime)
val request = Request.Builder()
.url(apiUrl)
.post(body)
.build()
return authClient.newCall(request)
return authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime)))
.asObservableSuccess()
.map { netResponse ->
val responseBody = netResponse.body?.string().orEmpty()
netResponse.close()
if (responseBody.isEmpty()) {
throw Exception("Null Response")
netResponse.use {
val responseBody = it.body?.string().orEmpty()
if (responseBody.isEmpty()) {
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())
}
}
val body = payload.toString().toRequestBody(jsonMime)
val request = Request.Builder()
.url(apiUrl)
.post(body)
.build()
return authClient.newCall(request)
return authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime)))
.asObservableSuccess()
.map {
track
@@ -134,24 +125,21 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("query", search)
}
}
val body = payload.toString().toRequestBody(jsonMime)
val request = Request.Builder()
.url(apiUrl)
.post(body)
.build()
return authClient.newCall(request)
return authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime)))
.asObservableSuccess()
.map { netResponse ->
val responseBody = netResponse.body?.string().orEmpty()
if (responseBody.isEmpty()) {
throw Exception("Null Response")
netResponse.use {
val responseBody = it.body?.string().orEmpty()
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)
}
}
val body = payload.toString().toRequestBody(jsonMime)
val request = Request.Builder()
.url(apiUrl)
.post(body)
.build()
return authClient.newCall(request)
return authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime)))
.asObservableSuccess()
.map { netResponse ->
val responseBody = netResponse.body?.string().orEmpty()
if (responseBody.isEmpty()) {
throw Exception("Null Response")
netResponse.use {
val responseBody = it.body?.string().orEmpty()
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 {
put("query", query)
}
val body = payload.toString().toRequestBody(jsonMime)
val request = Request.Builder()
.url(apiUrl)
.post(body)
.build()
return authClient.newCall(request)
return authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime)))
.asObservableSuccess()
.map { netResponse ->
val responseBody = netResponse.body?.string().orEmpty()
if (responseBody.isEmpty()) {
throw Exception("Null Response")
netResponse.use {
val responseBody = it.body?.string().orEmpty()
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 {
private const val clientId = "385"
private const val clientUrl = "tachiyomi://anilist-auth"
private const val apiUrl = "https://graphql.anilist.co/"
private const val baseUrl = "https://anilist.co/api/v2/"
private const val baseMangaUrl = "https://anilist.co/manga/"
@@ -63,7 +63,7 @@ data class ALUserManga(
"DROPPED" -> Anilist.DROPPED
"PLANNING" -> Anilist.PLANNING
"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.PLANNING -> "PLANNING"
Anilist.REPEATING -> "REPEATING"
else -> throw NotImplementedError("Unknown status")
else -> throw NotImplementedError("Unknown status: $status")
}
private val preferences: PreferencesHelper by injectLazy()
@@ -102,5 +102,5 @@ fun Track.toAnilistScore(): String = when (preferences.anilistScoreType().get())
}
// 10 point decimal
"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.track.TrackManager
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.asObservableSuccess
import kotlinx.serialization.decodeFromString
@@ -34,11 +35,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
.add("rating", track.score.toInt().toString())
.add("status", track.toBangumiStatus())
.build()
val request = Request.Builder()
.url("$apiUrl/collection/${track.media_id}/update")
.post(body)
.build()
return authClient.newCall(request)
return authClient.newCall(POST("$apiUrl/collection/${track.media_id}/update", body = body))
.asObservableSuccess()
.map {
track
@@ -46,29 +43,20 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
}
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
val sbody = FormBody.Builder()
.add("status", track.toBangumiStatus())
.build()
val srequest = Request.Builder()
.url("$apiUrl/collection/${track.media_id}/update")
.post(sbody)
.build()
return authClient.newCall(srequest)
return authClient.newCall(POST("$apiUrl/collection/${track.media_id}/update", body = sbody))
.asObservableSuccess()
.map {
track
}.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()
.map {
track
@@ -82,11 +70,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
.buildUpon()
.appendQueryParameter("max_results", "20")
.build()
val request = Request.Builder()
.url(url.toString())
.get()
.build()
return authClient.newCall(request)
return authClient.newCall(GET(url.toString()))
.asObservableSuccess()
.map { netResponse ->
var responseBody = netResponse.body?.string().orEmpty()
@@ -126,13 +110,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
}
fun findLibManga(track: Track): Observable<Track?> {
val urlMangas = "$apiUrl/subject/${track.media_id}"
val requestMangas = Request.Builder()
.url(urlMangas)
.get()
.build()
return authClient.newCall(requestMangas)
return authClient.newCall(GET("$apiUrl/subject/${track.media_id}"))
.asObservableSuccess()
.map { netResponse ->
// get comic info
@@ -162,13 +140,17 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
}
fun accessToken(code: String): Observable<OAuth> {
return client.newCall(accessTokenRequest(code)).asObservableSuccess().map { netResponse ->
val responseBody = netResponse.body?.string().orEmpty()
if (responseBody.isEmpty()) {
throw Exception("Null Response")
return client.newCall(accessTokenRequest(code))
.asObservableSuccess()
.map { netResponse ->
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(
@@ -8,7 +8,7 @@ fun Track.toBangumiStatus() = when (status) {
Bangumi.ON_HOLD -> "on_hold"
Bangumi.DROPPED -> "dropped"
Bangumi.PLANNING -> "wish"
else -> throw NotImplementedError("Unknown status")
else -> throw NotImplementedError("Unknown status: $status")
}
fun toTrackStatus(status: String) = when (status) {
@@ -17,6 +17,5 @@ fun toTrackStatus(status: String) = when (status) {
"on_hold" -> Bangumi.ON_HOLD
"dropped" -> Bangumi.DROPPED
"wish" -> Bangumi.PLANNING
else -> throw Exception("Unknown status")
else -> throw NotImplementedError("Unknown status: $status")
}
@@ -111,7 +111,7 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
fun ensureLoggedIn() {
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() {
@@ -12,7 +12,7 @@ import eu.kanade.tachiyomi.util.lang.toCalendar
import eu.kanade.tachiyomi.util.selectInt
import eu.kanade.tachiyomi.util.selectText
import okhttp3.FormBody
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
@@ -282,7 +282,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.put("score", track.score)
.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 {
@@ -19,9 +19,8 @@ import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonObject
import okhttp3.FormBody
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import rx.Observable
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 jsonime = "application/json; charset=utf-8".toMediaTypeOrNull()
private val jsonMime = "application/json; charset=utf-8".toMediaType()
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
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())
}
}
val body = payload.toString().toRequestBody(jsonime)
val request = Request.Builder()
.url("$apiUrl/v2/user_rates")
.post(body)
.build()
return authClient.newCall(request)
return authClient.newCall(POST("$apiUrl/v2/user_rates", body = payload.toString().toRequestBody(jsonMime)))
.asObservableSuccess()
.map {
track
@@ -64,11 +58,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
.appendQueryParameter("search", search)
.appendQueryParameter("limit", "20")
.build()
val request = Request.Builder()
.url(url.toString())
.get()
.build()
return authClient.newCall(request)
return authClient.newCall(GET(url.toString()))
.asObservableSuccess()
.map { netResponse ->
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?> {
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()
.appendPath(track.media_id.toString())
.build()
val requestMangas = Request.Builder()
.url(urlMangas.toString())
.get()
.build()
return authClient.newCall(requestMangas)
return authClient.newCall(GET(urlMangas.toString()))
.asObservableSuccess()
.map { netResponse ->
val responseBody = netResponse.body?.string().orEmpty()
json.decodeFromString<JsonObject>(responseBody)
}.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()
.map { netResponse ->
val responseBody = netResponse.body?.string().orEmpty()
@@ -155,13 +136,17 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
}
fun accessToken(code: String): Observable<OAuth> {
return client.newCall(accessTokenRequest(code)).asObservableSuccess().map { netResponse ->
val responseBody = netResponse.body?.string().orEmpty()
if (responseBody.isEmpty()) {
throw Exception("Null Response")
return client.newCall(accessTokenRequest(code))
.asObservableSuccess()
.map { netResponse ->
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(
@@ -9,7 +9,7 @@ fun Track.toShikimoriStatus() = when (status) {
Shikimori.DROPPED -> "dropped"
Shikimori.PLANNING -> "planned"
Shikimori.REPEATING -> "rewatching"
else -> throw NotImplementedError("Unknown status")
else -> throw NotImplementedError("Unknown status: $status")
}
fun toTrackStatus(status: String) = when (status) {
@@ -19,6 +19,5 @@ fun toTrackStatus(status: String) = when (status) {
"dropped" -> Shikimori.DROPPED
"planned" -> Shikimori.PLANNING
"rewatching" -> Shikimori.REPEATING
else -> throw Exception("Unknown status")
else -> throw NotImplementedError("Unknown status: $status")
}
@@ -37,11 +37,11 @@ class GithubUpdateChecker {
val newVersion = versionTag.replace("[^\\d.]".toRegex(), "")
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"
newVersion.toInt() > BuildConfig.COMMIT_COUNT.toInt()
} 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"
newVersion != BuildConfig.VERSION_NAME
}
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension
import android.content.Context
import android.graphics.drawable.Drawable
import androidx.core.content.ContextCompat
import com.elvishew.xlog.XLog
import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.R
@@ -76,9 +77,9 @@ class ExtensionManager(
// SY -->
return when (source.id) {
EH_SOURCE_ID -> context.getDrawable(R.mipmap.ic_ehentai_source)
EXH_SOURCE_ID -> context.getDrawable(R.mipmap.ic_ehentai_source)
MERGED_SOURCE_ID -> context.getDrawable(R.mipmap.ic_merged_source)
EH_SOURCE_ID -> ContextCompat.getDrawable(context, R.mipmap.ic_ehentai_source)
EXH_SOURCE_ID -> ContextCompat.getDrawable(context, R.mipmap.ic_ehentai_source)
MERGED_SOURCE_ID -> ContextCompat.getDrawable(context, R.mipmap.ic_merged_source)
else -> null
}
// SY <--
@@ -142,8 +143,7 @@ class ExtensionManager(
.map { it.extension }
installedExtensions
.flatMap { it.sources }
// overwrite is needed until the bundled sources are removed
.forEach { sourceManager.registerSource(it, true) }
.forEach { sourceManager.registerSource(it) }
untrustedExtensions = extensions
.filterIsInstance<LoadResult.Untrusted>()
@@ -25,7 +25,12 @@ internal class ExtensionGithubApi {
return withContext(Dispatchers.IO) {
val response = service.getRepo()
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> {
@@ -58,7 +63,7 @@ internal class ExtensionGithubApi {
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
.filter { element ->
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
@@ -73,20 +78,21 @@ internal class ExtensionGithubApi {
val versionCode = element.jsonObject["code"]!!.jsonPrimitive.int
val lang = element.jsonObject["lang"]!!.jsonPrimitive.content
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 {
return "$REPO_URL_PREFIX/apk/${extension.apkName}"
return /* SY --> */ "${extension.repoUrl}/apk/${extension.apkName}" /* SY <-- */
}
// SY -->
fun Extension.isBlacklisted(
blacklistEnabled: Boolean =
preferences.enableSourceBlacklist().get()
private fun Extension.isBlacklisted(
blacklistEnabled: Boolean = preferences.enableSourceBlacklist().get()
): Boolean {
return pkgName in BlacklistedSources.BLACKLISTED_EXTENSIONS && blacklistEnabled
}
@@ -94,6 +100,6 @@ internal class ExtensionGithubApi {
companion object {
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 retrofit2.Retrofit
import retrofit2.http.GET
import retrofit2.http.Url
import uy.kohesive.injekt.injectLazy
/**
@@ -27,6 +28,8 @@ interface ExtensionGithubService {
}
}
@GET("${ExtensionGithubApi.REPO_URL_PREFIX}index.min.json")
suspend fun getRepo(): JsonArray
// SY -->
@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 isNsfw: Boolean,
val apkName: String,
val iconUrl: String
val iconUrl: String,
// SY -->
val repoUrl: String
// SY <--
) : Extension()
data class Untrusted(
@@ -6,7 +6,6 @@ import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import dalvik.system.PathClassLoader
import eu.kanade.tachiyomi.annotations.Nsfw
import eu.kanade.tachiyomi.data.preference.PreferenceValues
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult
@@ -26,8 +25,8 @@ import uy.kohesive.injekt.injectLazy
internal object ExtensionLoader {
private val preferences: PreferencesHelper by injectLazy()
private val allowNsfwSource by lazy {
preferences.allowNsfwSource().get()
private val loadNsfwSource by lazy {
preferences.showNsfwSource().get()
}
private const val EXTENSION_FEATURE = "tachiyomi.extension"
@@ -133,7 +132,7 @@ internal object ExtensionLoader {
}
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")
}
@@ -218,7 +217,7 @@ internal object ExtensionLoader {
* Checks whether a Source or SourceFactory is annotated with @Nsfw.
*/
private fun isSourceNsfw(clazz: Any): Boolean {
if (allowNsfwSource == PreferenceValues.NsfwAllowance.ALLOWED) {
if (loadNsfwSource) {
return false
}
@@ -62,14 +62,18 @@ interface Source : tachiyomi.source.Source {
/**
* [1.x API] Get the updated details for a manga.
*/
@Suppress("DEPRECATION")
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
return fetchMangaDetails(manga.toSManga()).awaitSingle()
.toMangaInfo()
val sManga = manga.toSManga()
val networkManga = fetchMangaDetails(sManga).awaitSingle()
sManga.copyFrom(networkManga)
return sManga.toMangaInfo()
}
/**
* [1.x API] Get all the available chapters for a manga.
*/
@Suppress("DEPRECATION")
override suspend fun getChapterList(manga: MangaInfo): List<ChapterInfo> {
return fetchChapterList(manga.toSManga()).awaitSingle()
.map { it.toChapterInfo() }
@@ -78,6 +82,7 @@ interface Source : tachiyomi.source.Source {
/**
* [1.x API] Get the list of pages a chapter has.
*/
@Suppress("DEPRECATION")
override suspend fun getPageList(chapter: ChapterInfo): List<tachiyomi.source.model.Page> {
return fetchPageList(chapter.toSChapter()).awaitSingle()
.map { it.toPageUrl() }
@@ -103,7 +103,7 @@ open class SourceManager(private val context: Context) {
}
// SY <--
internal fun registerSource(source: Source, overwrite: Boolean = false) {
internal fun registerSource(source: Source) {
// EXH -->
val sourceQName = source::class.qualifiedName
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 <--
if (overwrite || !sourcesMap.containsKey(source.id)) {
if (!sourcesMap.containsKey(source.id)) {
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 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.size == 1 || {
if (mangaReferences.size == 1 || run {
val mangaReference = mangaReferences.firstOrNull()
mangaReference == null || (mangaReference.mangaSourceId == MERGED_SOURCE_ID)
}()
}
) throw IllegalArgumentException("Manga references contain only the merged reference, merge is likely corrupted")
emit(
@@ -4,25 +4,15 @@ import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding
import eu.kanade.tachiyomi.R
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 eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
abstract class BaseThemedActivity : AppCompatActivity() {
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 {
val themeMode = preferences.themeMode().get()
(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?) {
setTheme(
when {
@@ -77,13 +62,5 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
)
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.RestoreViewOnCreateController
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.clearFindViewByIdCache
import timber.log.Timber
abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
@@ -54,11 +53,6 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
return inflateView(inflater, container)
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
clearFindViewByIdCache()
}
abstract fun inflateView(inflater: LayoutInflater, container: ViewGroup): View
open fun onViewCreated(view: View) {}
@@ -5,11 +5,14 @@ import android.view.View
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
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.InstallStep
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.util.system.LocaleHelper
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
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 shouldLabelNsfw by lazy {
Injekt.get<PreferencesHelper>().labelNsfwExtension()
}
init {
binding.extButton.setOnClickListener {
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)
// SY -->
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 <--
extension.isNsfw -> itemView.context.getString(R.string.ext_nsfw_short)
else -> ""
}.toUpperCase()
GlideApp.with(itemView.context).clear(binding.image)
@@ -51,6 +58,23 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
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")
fun bindButton(item: ExtensionItem) = with(binding.extButton) {
isEnabled = true
@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.browse.extension
import android.app.Application
import android.os.Bundle
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferenceValues
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.model.Extension
@@ -56,7 +55,7 @@ open class ExtensionPresenter(
private fun toItems(tuple: ExtensionTuple): List<ExtensionItem> {
val context = Injekt.get<Application>()
val activeLangs = preferences.enabledLanguages().get()
val showNsfwExtensions = preferences.allowNsfwSource().get() != PreferenceValues.NsfwAllowance.BLOCKED
val showNsfwExtensions = preferences.showNsfwExtension().get()
val (installed, untrusted, available) = tuple
@@ -635,7 +635,6 @@ open class BrowseSourceController(bundle: Bundle) :
val adapter = adapter ?: return
preferences.sourceDisplayMode().set(mode)
presenter.refreshDisplayMode()
activity?.invalidateOptionsMenu()
setupRecycler(view)
@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.browse.source.browse
import android.os.Bundle
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.flexibleadapter.items.ISectionable
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
@@ -38,10 +37,13 @@ import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.removeCovers
import exh.savedsearches.EXHSavedSearch
import exh.savedsearches.JsonSavedSearch
import kotlinx.coroutines.Job
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.isActive
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
@@ -127,11 +129,6 @@ open class BrowseSourcePresenter(
private val filterSerializer = FilterSerializer()
// SY <--
/**
* Job to initialize manga details.
*/
private var initializerJob: Job? = null
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
@@ -169,8 +166,6 @@ open class BrowseSourcePresenter(
this.query = query
this.appliedFilters = filters
initializeManga()
// Create a new pager.
pager = createPager(query, filters)
@@ -226,27 +221,6 @@ open class BrowseSourcePresenter(
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
* if the manga is not yet in the database.
@@ -272,7 +246,19 @@ open class BrowseSourcePresenter(
* @param mangas the list of manga to initialize.
*/
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
*/
private suspend fun getMangaDetails(manga: Manga): Manga {
return try {
source.getMangaDetails(manga.toMangaInfo())
.let { networkManga ->
manga.copyFrom(networkManga.toSManga())
manga.initialized = true
db.insertManga(manga).executeAsBlocking()
manga
}
try {
val networkManga = source.getMangaDetails(manga.toMangaInfo())
manga.copyFrom(networkManga.toSManga())
manga.initialized = true
db.insertManga(manga).executeAsBlocking()
} catch (e: Exception) {
manga
Timber.e(e)
}
return manga
}
/**
@@ -316,13 +300,6 @@ open class BrowseSourcePresenter(
db.insertManga(manga).executeAsBlocking()
}
/**
* Refreshes the active display mode.
*/
fun refreshDisplayMode() {
initializeManga()
}
/**
* Set the filter states for the current source.
*
@@ -363,7 +340,7 @@ open class BrowseSourcePresenter(
is Filter.AutoComplete -> AutoCompleteSectionItem(it)
// SY <--
else -> null
} as? ISectionable<*, *>
}
}
subItems.forEach { it.header = group }
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
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import eu.kanade.tachiyomi.R
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.widget.RecyclerViewPagerAdapter
@@ -34,8 +35,9 @@ class LibraryAdapter(private val controller: LibraryController) : RecyclerViewPa
* @return a new view.
*/
override fun createView(container: ViewGroup): View {
val view = container.inflate(R.layout.library_category) as LibraryCategoryView
view.onCreate(controller)
val binding = LibraryCategoryBinding.inflate(LayoutInflater.from(container.context), container, false)
val view: LibraryCategoryView = binding.root
view.onCreate(controller, binding)
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.DisplayMode
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.LibraryCategoryBinding
import eu.kanade.tachiyomi.ui.category.CategoryAdapter
import eu.kanade.tachiyomi.util.lang.plusAssign
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.inflate
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
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.Dispatchers
import kotlinx.coroutines.Job
@@ -96,15 +95,15 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
}
// EXH <--
fun onCreate(controller: LibraryController) {
fun onCreate(controller: LibraryController, binding: LibraryCategoryBinding) {
this.controller = controller
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)
}
} 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
}
}
@@ -113,21 +112,21 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
recycler.setHasFixedSize(true)
recycler.adapter = adapter
swipe_refresh.addView(recycler)
adapter.fastScroller = fast_scroller
binding.swipeRefresh.addView(recycler)
adapter.fastScroller = binding.fastScroller
recycler.scrollStateChanges()
.onEach {
// Disable swipe refresh when view is not at the top
val firstPos = (recycler.layoutManager as LinearLayoutManager)
.findFirstCompletelyVisibleItemPosition()
swipe_refresh.isEnabled = firstPos <= 0
binding.swipeRefresh.isEnabled = firstPos <= 0
}
.launchIn(scope)
// Double the distance required to trigger sync
swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt())
swipe_refresh.refreshes()
binding.swipeRefresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt())
binding.swipeRefresh.refreshes()
.onEach {
// SY -->
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 <--
// 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)
}
@@ -174,7 +174,7 @@ class LibraryController(
if (preferences.categoryTabs().get()) {
currentTitle = resources?.getString(R.string.label_library)
} else {
adapter?.categories?.get(binding.libraryPager.currentItem)?.let {
adapter?.categories?.getOrNull(binding.libraryPager.currentItem)?.let {
currentTitle = it.name
}
}
@@ -684,7 +684,7 @@ class LibraryPresenter(
libraryManga.forEach { libraryItem ->
when (groupType) {
LibraryGroup.BY_TRACK_STATUS -> {
val status: String = {
val status: String = run {
val tracks = db.getTracks(libraryItem.manga).executeAsBlocking()
val track = tracks.find { track ->
loggedServices.any { it.id == track?.sync_id }
@@ -695,30 +695,30 @@ class LibraryPresenter(
} else {
"not tracked"
}
}()
}
val group = grouping.find { it.first == trackManager.mapTrackingOrder(status, context).toString() }
if (group != null) {
map[group.second]?.plusAssign(libraryItem) ?: map.put(group.second, mutableListOf(libraryItem))
map.getOrPut(group.second) { mutableListOf() } += libraryItem
} else {
map[7]?.plusAssign(libraryItem) ?: map.put(7, mutableListOf(libraryItem))
map.getOrPut(7) { mutableListOf() } += libraryItem
}
}
LibraryGroup.BY_SOURCE -> {
val group = grouping.find { it.first.toLongOrNull() == libraryItem.manga.source }
if (group != null) {
map[group.second]?.plusAssign(libraryItem) ?: map.put(group.second, mutableListOf(libraryItem))
map.getOrPut(group.second) { mutableListOf() } += libraryItem
} else {
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 -> {
val group = grouping.find { it.second == libraryItem.manga.status }
if (group != null) {
map[group.second]?.plusAssign(libraryItem) ?: map.put(group.second, mutableListOf(libraryItem))
map.getOrPut(group.second) { mutableListOf() } += libraryItem
} else {
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 }
else -> grouping
}
).map {
val category = Category.create(it.third)
category.id = it.second
category
}
)
.map {
val category = Category.create(it.third)
category.id = it.second
category
}
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.databinding.MainActivityBinding
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.FabController
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
@@ -54,7 +54,7 @@ import java.util.Date
import java.util.LinkedList
import java.util.concurrent.TimeUnit
class MainActivity : BaseActivity<MainActivityBinding>() {
class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
private lateinit var router: Router
@@ -62,17 +62,13 @@ class EditMangaDialog : DialogController {
}
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
binding = EditMangaDialogBinding.inflate(activity!!.layoutInflater)
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)
positiveButton(R.string.action_save) { onPositiveButtonClick() }
}
binding = EditMangaDialogBinding.bind(dialog.view.contentLayout.customView!!)
onViewCreated()
dialog.setOnShowListener {
val dView = (it as? MaterialDialog)?.view
dView?.contentLayout?.scrollView?.scrollTo(0, 0)
}
return dialog
}
@@ -1333,7 +1333,7 @@ class MangaController :
private fun markPreviousAsRead(chapters: List<ChapterItem>) {
val adapter = chaptersAdapter ?: return
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) {
markAsRead(prevChapters.take(chapterPos))
}
@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.manga.merged
import android.app.Dialog
import android.os.Bundle
import android.view.View
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager
import com.afollestad.materialdialogs.MaterialDialog
@@ -19,9 +18,6 @@ import exh.merged.sql.models.MergedMangaReference
import uy.kohesive.injekt.injectLazy
class EditMergedSettingsDialog : DialogController, EditMergedMangaAdapter.EditMergedMangaItemListener {
private var dialogView: View? = null
private val manga: Manga
val mergedMangas: MutableList<Pair<Manga?, MergedMangaReference>> = mutableListOf()
@@ -55,22 +51,17 @@ class EditMergedSettingsDialog : DialogController, EditMergedMangaAdapter.EditMe
private var mergedMangaAdapter: EditMergedMangaAdapter? = null
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
val dialog = MaterialDialog(activity!!).apply {
customView(viewRes = R.layout.edit_merged_settings_dialog, scrollable = true)
negativeButton(android.R.string.cancel)
positiveButton(R.string.action_save) { onPositiveButtonClick() }
}
dialogView = dialog.view
onViewCreated(dialog.view)
dialog.setOnShowListener {
val dView = (it as? MaterialDialog)?.view
dView?.contentLayout?.scrollView?.scrollTo(0, 0)
}
binding = EditMergedSettingsDialogBinding.inflate(activity!!.layoutInflater)
val dialog = MaterialDialog(activity!!)
.customView(view = binding.root, scrollable = true)
.negativeButton(android.R.string.cancel)
.positiveButton(R.string.action_save) { onPositiveButtonClick() }
onViewCreated()
return dialog
}
fun onViewCreated(view: View) {
binding = EditMergedSettingsDialogBinding.bind(view)
fun onViewCreated() {
val mergedManga = db.getMergedMangas(manga.id!!).executeAsBlocking()
val mergedReferences = db.getMergedMangaReferences(manga.id!!).executeAsBlocking()
if (mergedReferences.isEmpty() || mergedReferences.size == 1) {
@@ -86,18 +77,13 @@ class EditMergedSettingsDialog : DialogController, EditMergedMangaAdapter.EditMe
mergedHeaderAdapter = EditMergedSettingsHeaderAdapter(this, mergedMangaAdapter!!)
binding.recycler.adapter = ConcatAdapter(mergedHeaderAdapter, mergedMangaAdapter)
binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.layoutManager = LinearLayoutManager(activity!!)
mergedMangaAdapter?.isHandleDragEnabled = isPriorityOrder
mergedMangaAdapter?.updateDataSet(mergedMangas.map { it.toModel() })
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
dialogView = null
}
private fun onPositiveButtonClick() {
mangaController.presenter.updateMergeSettings(mergeReference, mergedMangas.map { it.second })
}
@@ -115,7 +101,7 @@ class EditMergedSettingsDialog : DialogController, EditMergedMangaAdapter.EditMe
val mergedMangaAdapter = mergedMangaAdapter ?: return
val mergeMangaReference = mergedMangaAdapter.currentItems.getOrNull(position)?.mergedMangaReference ?: return
MaterialDialog(dialogView!!.context)
MaterialDialog(activity!!)
.title(R.string.delete_merged_manga)
.message(R.string.delete_merged_manga_desc)
.positiveButton(android.R.string.ok) {
@@ -128,7 +114,7 @@ class EditMergedSettingsDialog : DialogController, EditMergedMangaAdapter.EditMe
}
override fun onToggleChapterUpdatesClicked(position: Int) {
MaterialDialog(dialogView!!.context)
MaterialDialog(activity!!)
.title(R.string.chapter_updates_merged_manga)
.message(R.string.chapter_updates_merged_manga_desc)
.positiveButton(android.R.string.ok) {
@@ -152,7 +138,7 @@ class EditMergedSettingsDialog : DialogController, EditMergedMangaAdapter.EditMe
}
override fun onToggleChapterDownloadsClicked(position: Int) {
MaterialDialog(dialogView!!.context)
MaterialDialog(activity!!)
.title(R.string.download_merged_manga)
.message(R.string.download_merged_manga_desc)
.positiveButton(android.R.string.ok) {
@@ -42,7 +42,7 @@ class EditMergedSettingsHeaderAdapter(private val controller: EditMergedSettings
android.R.layout.simple_spinner_item,
listOf(
"No dedupe",
"Dedupe by priority",
/*"Dedupe by priority",*/
"Show source with most chapters",
"Show source with highest chapter number"
)
@@ -54,8 +54,8 @@ class EditMergedSettingsHeaderAdapter(private val controller: EditMergedSettings
when (it.chapterSortMode) {
MergedMangaReference.CHAPTER_SORT_NO_DEDUPE -> 0
MergedMangaReference.CHAPTER_SORT_PRIORITY -> 1
MergedMangaReference.CHAPTER_SORT_MOST_CHAPTERS -> 2
MergedMangaReference.CHAPTER_SORT_HIGHEST_CHAPTER_NUMBER -> 3
MergedMangaReference.CHAPTER_SORT_MOST_CHAPTERS -> 1
MergedMangaReference.CHAPTER_SORT_HIGHEST_CHAPTER_NUMBER -> 2
else -> 0
}
)
@@ -69,9 +69,9 @@ class EditMergedSettingsHeaderAdapter(private val controller: EditMergedSettings
) {
controller.mergeReference?.chapterSortMode = when (position) {
0 -> MergedMangaReference.CHAPTER_SORT_NO_DEDUPE
1 -> MergedMangaReference.CHAPTER_SORT_PRIORITY
2 -> MergedMangaReference.CHAPTER_SORT_MOST_CHAPTERS
3 -> MergedMangaReference.CHAPTER_SORT_HIGHEST_CHAPTER_NUMBER
99 -> MergedMangaReference.CHAPTER_SORT_PRIORITY
1 -> MergedMangaReference.CHAPTER_SORT_MOST_CHAPTERS
2 -> MergedMangaReference.CHAPTER_SORT_HIGHEST_CHAPTER_NUMBER
else -> MergedMangaReference.CHAPTER_SORT_NO_DEDUPE
}
XLog.d(controller.mergeReference?.chapterSortMode)
@@ -35,9 +35,6 @@ import java.util.TimeZone
class AboutController : SettingsController() {
/**
* Checks for new releases
*/
private val updateChecker by lazy { GithubUpdateChecker() }
private val dateFormat: DateFormat = preferences.dateFormat()
@@ -119,7 +116,7 @@ class AboutController : SettingsController() {
preference {
key = "pref_about_label_original_tachiyomi_github"
title = "Original Tachiyomi GitHub "
val url = "https://github.com/inorichi/tachiyomi"
val url = "https://github.com/tachiyomiorg/tachiyomi"
summary = url
onClick {
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
@@ -130,7 +127,7 @@ class AboutController : SettingsController() {
preference {
key = "pref_about_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
onClick {
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)
// SY -->
binding.zoomOutWebtoon.bindToPreference(preferences.webtoonEnableZoomOut())
binding.cropBordersContinuesVertical.bindToPreference(preferences.cropBordersContinuesVertical())
// SY <--
}
@@ -20,7 +20,7 @@ class DirectoryPageLoader(val file: File) : PageLoader() {
override fun getPages(): Observable<List<ReaderPage>> {
return file.listFiles()
.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 ->
val streamFn = { FileInputStream(file) }
ReaderPage(i).apply {
@@ -8,7 +8,6 @@ import eu.kanade.tachiyomi.util.system.ImageUtil
import rx.Observable
import java.io.File
import java.nio.charset.StandardCharsets
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
/**
@@ -40,7 +39,7 @@ class ZipPageLoader(file: File) : PageLoader() {
override fun getPages(): Observable<List<ReaderPage>> {
return zip.entries().toList()
.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 ->
val streamFn = { zip.getInputStream(entry) }
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.
*/
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}")
activity.onPageSelected(page)
@@ -19,6 +19,10 @@ class WebtoonConfig(preferences: PreferencesHelper = Injekt.get()) : ViewerConfi
// SY -->
var enableZoomOut = false
private set
var continuesCropBorders = false
private set
var zoomPropertyChangedListener: ((Boolean) -> Unit)? = null
// SY <--
@@ -32,6 +36,9 @@ class WebtoonConfig(preferences: PreferencesHelper = Injekt.get()) : ViewerConfi
// SY -->
preferences.webtoonEnableZoomOut()
.register({ enableZoomOut = it }, { zoomPropertyChangedListener?.invoke(it) })
preferences.cropBordersContinuesVertical()
.register({ continuesCropBorders = it }, { imagePropertyChangedListener?.invoke() })
// SY <--
}
}
@@ -361,7 +361,7 @@ class WebtoonPageHolder(
setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_FIT_WIDTH)
setMinimumDpi(90)
setMinimumTileDpi(180)
setCropBorders(config.imageCropBorders)
setCropBorders(/* SY --> */ if (viewer.isContinuous) config.continuesCropBorders else /* SY <-- */ config.imageCropBorders)
setOnImageEventListener(
object : SubsamplingScaleImageView.DefaultOnImageEventListener() {
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.
*/
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}")
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_DPAD_RIGHT,
KeyEvent.KEYCODE_DPAD_LEFT,
KeyEvent.KEYCODE_DPAD_UP,
KeyEvent.KEYCODE_PAGE_UP -> if (isUp) scrollUp()
KeyEvent.KEYCODE_DPAD_LEFT,
KeyEvent.KEYCODE_DPAD_RIGHT,
KeyEvent.KEYCODE_DPAD_DOWN,
KeyEvent.KEYCODE_PAGE_DOWN -> if (isUp) scrollDown()
else -> return false
@@ -2,26 +2,26 @@ package eu.kanade.tachiyomi.ui.recent
import android.text.format.DateUtils
import android.view.View
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.RecentSectionItemBinding
import java.util.Date
class DateSectionItem(val date: Date) : AbstractHeaderItem<DateSectionItem.Holder>() {
class DateSectionItem(val date: Date) : AbstractHeaderItem<DateSectionItem.DateSectionItemHolder>() {
override fun getLayoutRes(): Int {
return R.layout.recent_section_item
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
return Holder(view, adapter)
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): DateSectionItemHolder {
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)
}
@@ -37,14 +37,14 @@ class DateSectionItem(val date: Date) : AbstractHeaderItem<DateSectionItem.Holde
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
val section_text: TextView = view.findViewById(R.id.section_text)
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 {
key = Keys.enableDoh
titleRes = R.string.pref_dns_over_https
summaryRes = R.string.pref_dns_over_https_summary
summaryRes = R.string.requires_app_restart
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.switchPreference
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.onEach
@@ -125,13 +124,11 @@ class SettingsBackupController : SettingsController() {
titleRes = R.string.pref_backup_directory
onClick {
val currentDir = preferences.backupsDirectory().get()
try {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
startActivityForResult(intent, CODE_BACKUP_DIR)
} catch (e: ActivityNotFoundException) {
// Fall back to custom picker on error
startActivityForResult(preferences.context.getFilePicker(currentDir), CODE_BACKUP_DIR)
activity?.toast(R.string.file_picker_error)
}
}
@@ -205,7 +202,7 @@ class SettingsBackupController : SettingsController() {
)
}
CODE_BACKUP_RESTORE -> {
uri?.path?.let { path ->
uri?.path?.let {
val fileName = DocumentFile.fromSingleUri(activity, uri)!!.name!!
when {
fileName.endsWith(".proto.gz") -> {
@@ -258,7 +255,6 @@ class SettingsBackupController : SettingsController() {
fun createBackup(flags: Int, type: Int) {
backupFlags = flags
val currentDir = preferences.backupsDirectory().get()
val code = when (type) {
BackupConst.BACKUP_TYPE_FULL -> CODE_FULL_BACKUP_CREATE
else -> CODE_LEGACY_BACKUP_CREATE
@@ -277,8 +273,7 @@ class SettingsBackupController : SettingsController() {
startActivityForResult(intent, code)
} catch (e: ActivityNotFoundException) {
// Handle errors where the Android ROM doesn't support the built in picker
startActivityForResult(preferences.context.getFilePicker(currentDir), code)
activity?.toast(R.string.file_picker_error)
}
}
@@ -2,10 +2,13 @@ package eu.kanade.tachiyomi.ui.setting
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
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.util.preference.defaultValue
import eu.kanade.tachiyomi.util.preference.infoPreference
import eu.kanade.tachiyomi.util.preference.onChange
import eu.kanade.tachiyomi.util.preference.onClick
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.switchPreference
import eu.kanade.tachiyomi.util.preference.titleRes
import kotlinx.coroutines.flow.launchIn
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
class SettingsBrowseController : SettingsController() {
@@ -81,6 +85,19 @@ class SettingsBrowseController : SettingsController() {
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 {
@@ -93,27 +110,29 @@ class SettingsBrowseController : SettingsController() {
}
}
// preferenceCategory {
// titleRes = R.string.pref_category_nsfw_content
//
// listPreference {
// key = Keys.allowNsfwSource
// titleRes = R.string.pref_allow_nsfw_sources
// entriesRes = arrayOf(
// R.string.pref_allow_nsfw_sources_allowed,
// R.string.pref_allow_nsfw_sources_allowed_multisource,
// R.string.pref_allow_nsfw_sources_blocked
// )
// entryValues = arrayOf(
// PreferenceValues.NsfwAllowance.ALLOWED.name,
// PreferenceValues.NsfwAllowance.PARTIAL.name,
// PreferenceValues.NsfwAllowance.BLOCKED.name
// )
// defaultValue = PreferenceValues.NsfwAllowance.ALLOWED.name
// summary = "%s"
// }
//
// infoPreference(R.string.parental_controls_info)
// }
preferenceCategory {
titleRes = R.string.pref_category_nsfw_content
switchPreference {
key = Keys.showNsfwSource
titleRes = R.string.pref_show_nsfw_source
summaryRes = R.string.requires_app_restart
defaultValue = true
}
switchPreference {
key = Keys.showNsfwExtension
titleRes = R.string.pref_show_nsfw_extension
defaultValue = true
}
switchPreference {
key = Keys.labelNsfwExtension
titleRes = R.string.pref_label_nsfw_extension
defaultValue = true
preferences.showNsfwExtension().asImmediateFlow { isVisible = it }.launchIn(scope)
}
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.switchPreference
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.onEach
import uy.kohesive.injekt.Injekt
@@ -203,12 +203,12 @@ class SettingsDownloadController : SettingsController() {
preferences.downloadsDirectory().set(path.toString())
}
fun customDirectorySelected(currentDir: String) {
fun customDirectorySelected() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
try {
startActivityForResult(intent, DOWNLOAD_DIR)
} 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 ->
val target = targetController as? SettingsDownloadController
if (position == externalDirs.lastIndex) {
target?.customDirectorySelected(currentDir)
target?.customDirectorySelected()
} else {
target?.predefinedDirectorySelected(text.toString())
}
@@ -317,6 +317,11 @@ class SettingsReaderController : SettingsController() {
summaryRes = R.string.tap_scroll_page_summary
defaultValue = false
}
switchPreference {
key = Keys.cropBordersContinuesVertical
titleRes = R.string.pref_crop_borders
defaultValue = false
}
}
// SY <--
@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.setting
import android.app.Activity
import android.content.Intent
import androidx.browser.customtabs.CustomTabsIntent
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.R
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.switchPreference
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 uy.kohesive.injekt.injectLazy
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
@@ -47,11 +46,9 @@ class SettingsTrackingController :
startActivity(MyAnimeListLoginActivity.newIntent(activity!!))
}
trackPreference(trackManager.aniList) {
val tabsIntent = CustomTabsIntent.Builder()
.setToolbarColor(context.getResourceColor(R.attr.colorPrimary))
.build()
tabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
tabsIntent.launchUrl(activity!!, AnilistApi.authUrl())
activity?.openInBrowser(AnilistApi.authUrl(), trackManager.aniList.getLogoColor()) {
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
}
}
trackPreference(trackManager.kitsu) {
val dialog = TrackLoginDialog(trackManager.kitsu, R.string.email)
@@ -59,18 +56,14 @@ class SettingsTrackingController :
dialog.showDialog(router)
}
trackPreference(trackManager.shikimori) {
val tabsIntent = CustomTabsIntent.Builder()
.setToolbarColor(context.getResourceColor(R.attr.colorPrimary))
.build()
tabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
tabsIntent.launchUrl(activity!!, ShikimoriApi.authUrl())
activity?.openInBrowser(ShikimoriApi.authUrl(), trackManager.shikimori.getLogoColor()) {
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
}
}
trackPreference(trackManager.bangumi) {
val tabsIntent = CustomTabsIntent.Builder()
.setToolbarColor(context.getResourceColor(R.attr.colorPrimary))
.build()
tabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
tabsIntent.launchUrl(activity!!, BangumiApi.authUrl())
activity?.openInBrowser(BangumiApi.authUrl(), trackManager.bangumi.getLogoColor()) {
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
}
}
}
preferenceCategory {
@@ -35,7 +35,7 @@ object SettingsSearchHelper {
* All subclasses of `SettingsController` should be listed here, in order to have their preferences searchable.
*/
// SY -->
private val settingControllersList: List<KClass<out SettingsController>> = {
private val settingControllersList: List<KClass<out SettingsController>> = run {
val controllers = mutableListOf(
SettingsAdvancedController::class,
SettingsBackupController::class,
@@ -55,7 +55,7 @@ object SettingsSearchHelper {
controllers += SettingsEhController::class
}
controllers
}()
}
// SY <--
/**
@@ -119,7 +119,7 @@ object SettingsSearchHelper {
(pref.title != null) -> {
// Is an actual preference
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}")
prefSearchResultList.add(
@@ -1,30 +1,14 @@
package eu.kanade.tachiyomi.ui.setting.track
import android.content.Intent
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 android.net.Uri
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy
class AnilistLoginActivity : AppCompatActivity() {
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))
class AnilistLoginActivity : BaseOAuthLoginActivity() {
override fun handleResult(data: Uri?) {
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) {
trackManager.aniList.login(matchResult.groups[1]!!.value)
.subscribeOn(Schedulers.io())
@@ -42,12 +26,4 @@ class AnilistLoginActivity : AppCompatActivity() {
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
import android.content.Intent
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 android.net.Uri
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy
class BangumiLoginActivity : AppCompatActivity() {
class BangumiLoginActivity : 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))
val code = intent.data?.getQueryParameter("code")
override fun handleResult(data: Uri?) {
val code = data?.getQueryParameter("code")
if (code != null) {
trackManager.bangumi.login(code)
.subscribeOn(Schedulers.io())
@@ -41,12 +25,4 @@ class BangumiLoginActivity : AppCompatActivity() {
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
import android.content.Intent
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 android.net.Uri
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy
class ShikimoriLoginActivity : AppCompatActivity() {
class ShikimoriLoginActivity : 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))
val code = intent.data?.getQueryParameter("code")
override fun handleResult(data: Uri?) {
val code = data?.getQueryParameter("code")
if (code != null) {
trackManager.shikimori.login(code)
.subscribeOn(Schedulers.io())
@@ -41,12 +25,4 @@ class ShikimoriLoginActivity : AppCompatActivity() {
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.R
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.setDefaultSettings
import eu.kanade.tachiyomi.util.system.toast
@@ -19,7 +19,7 @@ import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.appcompat.navigationClicks
import reactivecircus.flowbinding.swiperefreshlayout.refreshes
open class BaseWebViewActivity : BaseActivity<WebviewActivityBinding>() {
open class BaseWebViewActivity : BaseViewBindingActivity<WebviewActivityBinding>() {
internal var bundle: Bundle? = null
@@ -31,15 +31,17 @@ open class BaseWebViewActivity : BaseActivity<WebviewActivityBinding>() {
if (!WebViewUtil.supportsWebView(this)) {
toast(R.string.information_webview_required, Toast.LENGTH_LONG)
finish()
return
}
try {
binding = WebviewActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
} catch (e: Exception) {
} catch (e: Throwable) {
// Potentially throws errors like "Error inflating class android.webkit.WebView"
toast(R.string.information_webview_required, Toast.LENGTH_LONG)
finish()
return
}
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) =
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> {
override fun onNext(t: T) {
offer(t)
@@ -199,7 +199,7 @@ fun <T : Any> Observable<T>.asFlow(): Flow<T> = callbackFlow {
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(
{ emitter ->
/*
@@ -13,12 +13,14 @@ import android.content.pm.PackageManager
import android.content.res.Resources
import android.graphics.Color
import android.net.ConnectivityManager
import android.net.Uri
import android.os.PowerManager
import android.view.View
import android.widget.Toast
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
import androidx.annotation.StringRes
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
@@ -29,10 +31,8 @@ import androidx.core.graphics.green
import androidx.core.graphics.red
import androidx.core.net.toUri
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.nononsenseapps.filepicker.FilePickerActivity
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.lang.truncateCenter
import eu.kanade.tachiyomi.widget.CustomLayoutPickerActivity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@@ -109,19 +109,6 @@ fun Context.notification(channelId: String, block: (NotificationCompat.Builder.(
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.
*
@@ -250,12 +237,21 @@ fun Context.isServiceRunning(serviceClass: Class<*>): Boolean {
/**
* 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 {
val intent = CustomTabsIntent.Builder()
.setToolbarColor(getResourceColor(R.attr.colorPrimary))
.setDefaultColorSchemeParams(
CustomTabColorSchemeParams.Builder()
.setToolbarColor(toolbarColor ?: getResourceColor(R.attr.colorPrimary))
.build()
)
.build()
intent.launchUrl(this, url.toUri())
block(intent)
intent.launchUrl(this, uri)
} catch (e: Exception) {
toast(e.message)
}
@@ -13,7 +13,7 @@ object WebViewUtil {
const val REQUESTED_WITH = "com.android.browser"
const val MINIMUM_WEBVIEW_VERSION = 84
const val MINIMUM_WEBVIEW_VERSION = 86
fun supportsWebView(context: Context): Boolean {
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.models.Chapter
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.online.UrlImportableSource
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 exh.source.getMainSource
import exh.util.executeOnIO
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
class GalleryAdder {
@@ -22,6 +25,11 @@ class GalleryAdder {
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()
fun pickSource(url: String): List<UrlImportableSource> {
@@ -30,7 +38,7 @@ class GalleryAdder {
.map { it.getMainSource() }
.filterIsInstance<UrlImportableSource>()
.filter {
try {
it.lang in filters.first && it.id !in filters.second && try {
it.matchesUri(uri)
} catch (e: Exception) {
false
@@ -63,7 +71,7 @@ class GalleryAdder {
.map { it.getMainSource() }
.filterIsInstance<UrlImportableSource>()
.find {
try {
it.lang in filters.first && it.id !in filters.second && try {
it.matchesUri(uri)
} catch (e: Exception) {
false
+10 -11
View File
@@ -35,18 +35,17 @@ object MetadataUtil {
private const val GB_FACTOR = 1000 * MB_FACTOR
private const val GIB_FACTOR = 1024 * MIB_FACTOR
fun parseHumanReadableByteCount(arg0: String): Double? {
val spaceNdx = arg0.indexOf(" ")
val ret = java.lang.Double.parseDouble(arg0.substring(0, spaceNdx))
when (arg0.substring(spaceNdx + 1)) {
"GB" -> return ret * GB_FACTOR
"GiB" -> return ret * GIB_FACTOR
"MB" -> return ret * MB_FACTOR
"MiB" -> return ret * MIB_FACTOR
"KB" -> return ret * KB_FACTOR
"KiB" -> return ret * KIB_FACTOR
fun parseHumanReadableByteCount(bytes: String): Double? {
val ret = bytes.substringBefore(' ').toDouble()
return when (bytes.substringAfter(' ')) {
"GB" -> ret * GB_FACTOR
"GiB" -> ret * GIB_FACTOR
"MB" -> ret * MB_FACTOR
"MiB" -> ret * MIB_FACTOR
"KB" -> ret * KB_FACTOR
"KiB" -> ret * KIB_FACTOR
else -> null
}
return null
}
val ONGOING_SUFFIX = arrayOf(
@@ -59,8 +59,8 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
titleObj?.let { manga.title = it }
// Set artist (if we can find one)
tags.filter { it.namespace == EH_ARTIST_NAMESPACE }.let {
if (it.isNotEmpty()) manga.artist = it.joinToString(transform = { it.name })
tags.filter { it.namespace == EH_ARTIST_NAMESPACE }.let { tags ->
if (tags.isNotEmpty()) manga.artist = tags.joinToString(transform = { it.name })
}
// Copy tags -> genres
@@ -77,35 +77,7 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
}
}
// Build a nice looking description out of what we know
/* 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")*/
manga.description = "meta"
}
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
@@ -29,14 +29,7 @@ class EightMusesSearchMetadata : RaisedSearchMetadata() {
manga.genre = tagsToGenreString()
/*val titleDesc = StringBuilder()
title?.let { titleDesc += "Title: $it\n" }
val tagsDesc = tagsToDescription()*/
manga.description = "meta" /*listOf(titleDesc.toString(), tagsDesc.toString())
.filter(String::isNotBlank)
.joinToString(separator = "\n")*/
manga.description = "meta"
}
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
@@ -37,15 +37,7 @@ class HBrowseSearchMetadata : RaisedSearchMetadata() {
manga.genre = tagsToGenreString()
/*val titleDesc = StringBuilder()
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")*/
manga.description = "meta"
}
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
@@ -36,14 +36,7 @@ class HentaiCafeSearchMetadata : RaisedSearchMetadata() {
manga.genre = tagsToGenreString()
/* val detailsDesc = "Title: $title\n" +
"Artist: $artist\n"
val tagsDesc = tagsToDescription()*/
manga.description = "meta" /*listOf(detailsDesc, tagsDesc.toString())
.filter(String::isNotBlank)
.joinToString(separator = "\n")*/
manga.description = "meta"
}
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
@@ -51,45 +51,7 @@ class HitomiSearchMetadata : RaisedSearchMetadata() {
manga.status = SManga.UNKNOWN
/*val titleDesc = StringBuilder()
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")*/
manga.description = "meta"
}
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
@@ -53,8 +53,8 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() {
}
// Set artist (if we can find one)
tags.filter { it.namespace == NHENTAI_ARTIST_NAMESPACE }.let {
if (it.isNotEmpty()) manga.artist = it.joinToString(transform = { it.name })
tags.filter { it.namespace == NHENTAI_ARTIST_NAMESPACE }.let { tags ->
if (tags.isNotEmpty()) manga.artist = tags.joinToString(transform = { it.name })
}
// Copy tags -> genres
@@ -56,41 +56,7 @@ class PervEdenSearchMetadata : RaisedSearchMetadata() {
// Copy tags -> genres
manga.genre = tagsToGenreString()
/*val titleDesc = StringBuilder()
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")*/
manga.description = "meta"
}
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
@@ -45,21 +45,7 @@ class PururinSearchMetadata : RaisedSearchMetadata() {
manga.genre = tagsToGenreString()
/*val titleDesc = StringBuilder()
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")*/
manga.description = "meta"
}
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
@@ -52,32 +52,7 @@ class TsuminoSearchMetadata : RaisedSearchMetadata() {
// Copy tags -> genres
manga.genre = tagsToGenreString()
/*val titleDesc = "Title: $title\n"
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")*/
manga.description = "meta"
}
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.JsonObject
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
@@ -103,10 +104,21 @@ class Anilist : API("https://graphql.anilist.co/") {
}
private fun getTitle(obj: JsonObject): String {
return obj["title"]!!.jsonObject.let {
it["romaji"]?.jsonPrimitive?.content
?: it["english"]?.jsonPrimitive?.content
?: it["native"]!!.jsonPrimitive.content
val titleObj = obj["title"]!!.jsonObject
val english = titleObj["english"]?.jsonPrimitive?.contentOrNull
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 {
|node {
|mediaRecommendation {
|countryOfOrigin
|siteUrl
|title {
|romaji
|english
|native
|}
|synonyms
|coverImage {
|large
|}
@@ -203,7 +217,7 @@ open class RecommendsPager(
recs
} catch (e: Exception) {
Timber.tag("RECOMMENDATIONS").e("%s > Error: %s", key, e.message)
listOf()
listOf<SMangaImpl>()
}
}
.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.databinding.EhActivityInterceptBinding
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.manga.MangaController
import exh.GalleryAddEvent
@@ -23,7 +23,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
class InterceptActivity : BaseActivity<EhActivityInterceptBinding>() {
class InterceptActivity : BaseViewBindingActivity<EhActivityInterceptBinding>() {
private var statusJob: Job? = null
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.source.Source
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.manga.MangaController
import exh.metadata.metadata.base.FlatMetadata
import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.source.getMainSource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@@ -74,13 +71,6 @@ class MetadataViewController : NucleusController<MetadataViewControllerBinding,
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?) {
val context = view?.context ?: return
data = meta?.getExtraInfoPairs(context).orEmpty()
@@ -1,17 +1,23 @@
package exh.ui.metadata
import android.os.Bundle
import com.elvishew.xlog.XLog
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.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.RaisedSearchMetadata
import exh.metadata.metadata.base.getFlatMetadataForManga
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import exh.source.getMainSource
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.api.get
@@ -20,29 +26,31 @@ class MetadataViewPresenter(
val source: Source,
val preferences: PreferencesHelper = 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?) {
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()
.observeOn(AndroidSchedulers.mainThread())
.subscribeLatestCache({ view, _ -> view.onNextMangaInfo(meta) })
meta
.onEachView { view, metadata ->
view.onNextMangaInfo(metadata)
}
.launchIn(scope)
}
private fun getMangaObservable(): Observable<Manga> {
return db.getManga(manga.url, manga.source).asRxObservable()
}
private fun getMangaMetaObservable(): Observable<FlatMetadata?> {
val mangaId = manga.id
return if (mangaId != null) {
db.getFlatMetadataForManga(mangaId).asRxObservable()
.observeOn(AndroidSchedulers.mainThread())
} else Observable.just(null)
private fun getMangaMetaObservable(): Flow<FlatMetadata?> {
return db.getFlatMetadataForManga(manga.id!!).asRxObservable().asFlow()
}
}
@@ -3,7 +3,6 @@ package exh.widget.preference
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import com.afollestad.materialdialogs.MaterialDialog
@@ -40,7 +39,7 @@ class MangadexLoginDialog(bundle: Bundle? = null) : DialogController(bundle) {
val scope = CoroutineScope(Job() + Dispatchers.Main)
var binding: PrefSiteLoginTwoFactorAuthBinding? = null
lateinit var binding: PrefSiteLoginTwoFactorAuthBinding
constructor(source: MangaDex) : this(
bundleOf(
@@ -51,31 +50,31 @@ class MangadexLoginDialog(bundle: Bundle? = null) : DialogController(bundle) {
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
binding = PrefSiteLoginTwoFactorAuthBinding.inflate(LayoutInflater.from(activity!!))
val dialog = MaterialDialog(activity!!)
.customView(view = binding!!.root, scrollable = false)
.customView(view = binding.root, scrollable = false)
onViewCreated(dialog.view)
onViewCreated()
return dialog
}
fun onViewCreated(view: View) {
binding!!.login.setMode(ActionProcessButton.Mode.ENDLESS)
binding!!.login.setOnClickListener { checkLogin() }
fun onViewCreated() {
binding.login.setMode(ActionProcessButton.Mode.ENDLESS)
binding.login.setOnClickListener { checkLogin() }
setCredentialsOnView()
binding!!.twoFactorCheck.setOnCheckedChangeListener { _, isChecked ->
binding!!.twoFactorHolder.isVisible = isChecked
binding.twoFactorCheck.setOnCheckedChangeListener { _, isChecked ->
binding.twoFactorHolder.isVisible = isChecked
}
}
private fun setCredentialsOnView() {
binding?.username?.setText(service.getUsername())
binding?.password?.setText(service.getPassword())
binding.username.setText(service.getUsername())
binding.password.setText(service.getPassword())
}
private fun checkLogin() {
binding?.apply {
with(binding) {
if (username.text.isNullOrBlank() || password.text.isNullOrBlank() || (twoFactorCheck.isChecked && twoFactorEdit.text.isNullOrBlank())) {
errorResult()
root.context.toast(R.string.fields_cannot_be_blank)
@@ -110,7 +109,7 @@ class MangadexLoginDialog(bundle: Bundle? = null) : DialogController(bundle) {
}
private fun errorResult() {
binding?.apply {
with(binding) {
dialog?.setCancelable(true)
dialog?.setCanceledOnTouchOutside(true)
login.progress = -1
@@ -127,7 +126,6 @@ class MangadexLoginDialog(bundle: Bundle? = null) : DialogController(bundle) {
private fun onDialogClosed() {
scope.cancel()
binding = null
if (activity != null) {
(activity as? Listener)?.siteLoginDialogClosed(source!!)
} 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