mirror of
https://github.com/mihonapp/mihon.git
synced 2025-08-12 01:31:32 +02:00
Compare commits
155 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
3eec207166 | ||
|
b5d83bdb56 | ||
|
2c495c4119 | ||
|
7c72d6cb7c | ||
|
8362bf0886 | ||
|
1a8155c45b | ||
|
3f2f946019 | ||
|
2c14a8dee1 | ||
|
917a283bd1 | ||
|
3e403d5ab3 | ||
|
746d35b52b | ||
|
9a7a03e327 | ||
|
a051079c6a | ||
|
7b3c18bb97 | ||
|
52daf3d58c | ||
|
f41bde5ee1 | ||
|
6151318ac1 | ||
|
b45c322729 | ||
|
b00e8768dc | ||
|
156feb6e8e | ||
|
e942b8a402 | ||
|
abdb67a123 | ||
|
ee20787c5e | ||
|
ec4e631760 | ||
|
02b430a5bf | ||
|
7878053df2 | ||
|
12a593c3c6 | ||
|
6b1f130750 | ||
|
bde4c0a648 | ||
|
5ae4621da1 | ||
|
5ea8d0546e | ||
|
8a064c118f | ||
|
2f91c27df2 | ||
|
763bd54707 | ||
|
0ea3cc7ce4 | ||
|
0de3558ab3 | ||
|
069f4e12d8 | ||
|
ae4dfc9956 | ||
|
ee711dc0fb | ||
|
c316e7faab | ||
|
7083b3d912 | ||
|
2d3a1b6a9e | ||
|
0df23ab878 | ||
|
7ed8de2ef4 | ||
|
d935e22f0d | ||
|
0e26abf7a6 | ||
|
59aef13200 | ||
|
9d1f6c4416 | ||
|
b9f7660a91 | ||
|
18b5250ed1 | ||
|
f683f21ee2 | ||
|
bd033db84c | ||
|
ab036312a4 | ||
|
634da15191 | ||
|
cea1720ea0 | ||
|
3f2f542265 | ||
|
b77edb2b5b | ||
|
1b699bb814 | ||
|
333c035fed | ||
|
ce29914c56 | ||
|
70e5361146 | ||
|
e7d6dfff53 | ||
|
eebfad5a95 | ||
|
77c0a93ac6 | ||
|
63a3e126b3 | ||
|
3ea84cf0ce | ||
|
7fa80ae556 | ||
|
925f71af15 | ||
|
c666dd623d | ||
|
2cd8733212 | ||
|
4b2a9bc621 | ||
|
12a9d0575d | ||
|
edcfa28b0b | ||
|
3155829994 | ||
|
d25707554e | ||
|
38df44ef4b | ||
|
df683375b1 | ||
|
cc3cbbc4bb | ||
|
6922394b8e | ||
|
24fd82d773 | ||
|
57aefcd917 | ||
|
b3854ad382 | ||
|
5f5fc77877 | ||
|
0493e77cff | ||
|
6240fe1dfc | ||
|
beb7f90908 | ||
|
a3917972b4 | ||
|
7094fef37f | ||
|
0f41e56a24 | ||
|
52b283283f | ||
|
ebb15bf96c | ||
|
6c527d52fb | ||
|
b8ea57e097 | ||
|
909aed4262 | ||
|
4d2fff9538 | ||
|
9a45983f17 | ||
|
11926014da | ||
|
72002c13d6 | ||
|
6ed767ae84 | ||
|
3826b307f7 | ||
|
887b157056 | ||
|
d36dd39743 | ||
|
dd008bc13a | ||
|
50b282f58b | ||
|
f8a7efbce7 | ||
|
7d2caeb270 | ||
|
708e71a35a | ||
|
4eaccc966e | ||
|
3670d649b8 | ||
|
90ab04e81d | ||
|
26b8df5354 | ||
|
11a8046c5f | ||
|
da16110e1c | ||
|
914b686c8e | ||
|
27133520fc | ||
|
24b967ad5c | ||
|
ca4b4a3f1e | ||
|
faef35ec47 | ||
|
326d4c2641 | ||
|
83436c9550 | ||
|
2084822731 | ||
|
071bad1232 | ||
|
ae1a76da2b | ||
|
fbc6965c4e | ||
|
57a5862840 | ||
|
91fbccdbaa | ||
|
0ab0dd95ae | ||
|
bc41040fd3 | ||
|
4c8dfd0c0c | ||
|
2b9dbfb390 | ||
|
84d546b724 | ||
|
63053b9940 | ||
|
2256030a2a | ||
|
79da33b597 | ||
|
7d67450e58 | ||
|
8aa11951bf | ||
|
f23f22ab01 | ||
|
96a64c7bd2 | ||
|
d1bb0fdf1d | ||
|
feca30d7ed | ||
|
b650151693 | ||
|
bb3afd0dc9 | ||
|
5e77ae208d | ||
|
24e5a4d7ec | ||
|
1d10d29fa9 | ||
|
9b00e91773 | ||
|
cd73c30d6f | ||
|
7bbba0c7d9 | ||
|
7907a4fc24 | ||
|
2f94f62a56 | ||
|
85791a9336 | ||
|
a4eba50cfd | ||
|
03980b2f27 | ||
|
664e5cfb59 | ||
|
b9736df7e0 |
.github
CODE_OF_CONDUCT.mdREADME.mdapp
build.gradle.kts
build.gradle.ktssrc
main
AndroidManifest.xml
java
eu
kanade
tachiyomi
App.ktAppModule.ktMigrations.kt
data
backup
coil
database
download
library
notification
preference
track
anilist
bangumi
job
kitsu
komga
model
myanimelist
shikimori
updater
extension
network
source
ui
base
activity
controller
browse
BrowseController.kt
extension
migration
manga
search
sources
source
category
library
ChangeMangaCategoriesDialog.ktDeleteLibraryMangasDialog.ktLibraryComfortableGridHolder.ktLibraryCompactGridHolder.ktLibraryController.ktLibraryListHolder.ktLibraryPresenter.kt
main
manga
MangaController.ktMangaPresenter.kt
chapter
ChapterDownloadView.ktChapterHolder.ktChaptersAdapter.ktDownloadCustomChaptersDialog.ktMangaChaptersHeaderAdapter.ktSetChapterSettingsDialog.kt
info
track
more
reader
PageIndicatorTextView.ktReaderActivity.ktReaderNavigationOverlayView.ktReaderPageSheet.ktReaderPresenter.ktReaderSeekBar.kt
setting
viewer
recent
security
setting
util
chapter
lang
preference
storage
system
AnimationExtensions.ktAuthenticatorUtil.ktContextExtensions.ktGLUtil.ktImageUtil.ktLocaleHelper.ktNotificationExtensions.kt
view
widget
ActionToolbar.ktElevationAppBarLayout.ktEmptyView.ktExtendedNavigationView.ktMaterialFastScroll.ktMaterialSpinnerView.ktRecyclerViewPagerAdapter.ktSimpleNavigationView.ktTachiyomiChangeHandlerFrameLayout.ktTachiyomiCoordinatorLayout.ktTachiyomiFullscreenDialog.ktTachiyomiScrollingViewBehavior.ktTachiyomiSearchView.ktTachiyomiTextInputEditText.ktThemedSwipeRefreshLayout.kt
materialdialogs
MaterialAlertDialogBuilderExtensions.ktQuadStateMultiChoiceDialogAdapter.ktQuadStateMultiChoiceViewHolder.ktQuadStateTextView.kt
preference
res
drawable
ic_library_outline_24dp.xmlic_palette_24dp.xmlic_save_24dp.xmlic_status_completed_24dp.xmlic_status_licensed_24dp.xmlic_status_ongoing_24dp.xmlic_status_unknown_24dp.xmlic_travel_explore_24dp.xmlmanga_backdrop_gradient.xmlmanga_info_gradient.xmlmanga_info_more_gradient.xmloval.xml
layout-sw720dp
layout
action_toolbar.xmldialog_stub_textinput.xmldownload_custom_amount.xmlextension_detail_header.xmlglobal_search_controller_card_item.xmlhistory_item.xmllicenses_controller.xmllicenses_item.xmlmain_activity.xmlmanga_full_cover_dialog.xmlmanga_info_header.xmlnavigation_view_text.xmlpref_account_login.xmlpref_theme_item.xmlpref_themes_list.xmlpref_tracker_item.xmlpref_widget_imageview.xmlreader_activity.xmlreader_color_filter_settings.xmlreader_page_sheet.xmlreader_transition_view.xmlsource_comfortable_grid_item.xmlsource_compact_grid_item.xmlsource_list_item.xmlsource_main_controller_item.xmltrack_search_dialog.xmltrack_search_item.xmlupdates_item.xml
menu
browse_extensions.xmlbrowse_sources.xmlfull_cover.xmlglobal_search.xmlhistory.xmllibrary.xmlmanga.xmlsettings_main.xmlsource_browse.xmltrack_search.xml
values-am
values-ar
values-b+es+419
values-be
values-bg
values-bn
values-ca
values-cs
values-cv
values-de
values-el
values-eo
values-es
values-fa
values-fi
values-fil
values-fr
values-gl
values-he
values-hi
values-hr
values-hu
values-in
values-it
values-ja
values-jv
values-ka-rGE
values-kn
values-ko
values-lv
values-mr
values-ms
values-nb-rNO
values-ne
values-night
values-nl
values-pl
values-pt-rBR
values-pt
values-ro
values-ru
values-sah
values-sc
values-sk
values-sr
values-sv
values-th
values-tr
values-uk
values-v28
values-vi
values-zh-rCN
values-zh-rTW
values
xml
standard
test
java
eu
kanade
tachiyomi
data
buildSrc/src/main/kotlin
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@@ -3,7 +3,7 @@
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated:
|
||||
- To the latest version of the app (stable is v0.11.1)
|
||||
- To the latest version of the app (stable is v0.12.2)
|
||||
- All extensions
|
||||
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||
|
102
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
102
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
@@ -3,57 +3,6 @@ description: Report an issue in Tachiyomi
|
||||
labels: [Bug]
|
||||
body:
|
||||
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
||||
options:
|
||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
||||
required: true
|
||||
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
|
||||
required: true
|
||||
- label: I have updated the app to version **[0.11.1](https://github.com/tachiyomiorg/tachiyomi/releases/tag/v0.11.1)**.
|
||||
required: true
|
||||
- label: I have updated all installed extensions.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: tachiyomi-version
|
||||
attributes:
|
||||
label: Tachiyomi version
|
||||
description: You can find your Tachiyomi version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "0.11.1"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: android-version
|
||||
attributes:
|
||||
label: Android version
|
||||
description: You can find this somewhere in your Android settings.
|
||||
placeholder: |
|
||||
Example: "Android 11"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: device
|
||||
attributes:
|
||||
label: Device
|
||||
description: List your device and model.
|
||||
placeholder: |
|
||||
Example: "Google Pixel 5"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduce-steps
|
||||
attributes:
|
||||
@@ -98,9 +47,60 @@ body:
|
||||
placeholder: |
|
||||
You can paste the crash logs in pure text or upload it as an attachment.
|
||||
|
||||
- type: input
|
||||
id: tachiyomi-version
|
||||
attributes:
|
||||
label: Tachiyomi version
|
||||
description: You can find your Tachiyomi version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "0.12.2"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: android-version
|
||||
attributes:
|
||||
label: Android version
|
||||
description: You can find this somewhere in your Android settings.
|
||||
placeholder: |
|
||||
Example: "Android 11"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: device
|
||||
attributes:
|
||||
label: Device
|
||||
description: List your device and model.
|
||||
placeholder: |
|
||||
Example: "Google Pixel 5"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: other-details
|
||||
attributes:
|
||||
label: Other details
|
||||
placeholder: |
|
||||
Additional details and attachments.
|
||||
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
||||
options:
|
||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
||||
required: true
|
||||
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
|
||||
required: true
|
||||
- label: I have updated the app to version **[0.12.2](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||
required: true
|
||||
- label: I have updated all installed extensions.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
34
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
34
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
@@ -3,23 +3,6 @@ description: Suggest a feature to improve Tachiyomi
|
||||
labels: [Feature request]
|
||||
body:
|
||||
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
||||
options:
|
||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
||||
required: true
|
||||
- label: I have updated the app to version **[0.11.1](https://github.com/tachiyomiorg/tachiyomi/releases/tag/v0.11.1)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
attributes:
|
||||
@@ -37,3 +20,20 @@ body:
|
||||
label: Other details
|
||||
placeholder: |
|
||||
Additional details and attachments.
|
||||
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
||||
options:
|
||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
||||
required: true
|
||||
- label: I have updated the app to version **[0.12.2](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
12
.github/pull_request_template.md
vendored
Normal file
12
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
<!--
|
||||
Please include a summary of the change and which issue is fixed.
|
||||
Also make sure you've tested your code and also done a self-review of it.
|
||||
Don't forget to check all base themes and tablet mode for relevant changes.
|
||||
|
||||
If your changes are visual, please provide images below:
|
||||
|
||||
### Images
|
||||
| Image 1 | Image 2 |
|
||||
| ------- | ------- |
|
||||
|  |  |
|
||||
-->
|
BIN
.github/readme-images/screens.png
vendored
BIN
.github/readme-images/screens.png
vendored
Binary file not shown.
Before ![]() (image error) Size: 454 KiB |
33
.github/workflows/build_pull_request.yml
vendored
Normal file
33
.github/workflows/build_pull_request.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: PR build check
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build app
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Clone repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 11
|
||||
|
||||
- name: Copy CI gradle.properties
|
||||
run: |
|
||||
mkdir -p ~/.gradle
|
||||
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
||||
|
||||
- name: Build app
|
||||
uses: gradle/gradle-command-action@v1
|
||||
with:
|
||||
arguments: assembleStandardRelease
|
||||
distributions-cache-enabled: true
|
||||
dependencies-cache-enabled: true
|
||||
configuration-cache-enabled: true
|
@@ -5,23 +5,10 @@ on:
|
||||
- master
|
||||
tags:
|
||||
- v*
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
check_wrapper:
|
||||
name: Validate Gradle Wrapper
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Clone repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
build:
|
||||
name: Build app
|
||||
needs: check_wrapper
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -33,6 +20,9 @@ jobs:
|
||||
- name: Clone repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
@@ -44,10 +34,10 @@ jobs:
|
||||
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
||||
|
||||
- name: Build app
|
||||
uses: eskatos/gradle-command-action@v1
|
||||
uses: gradle/gradle-command-action@v1
|
||||
with:
|
||||
arguments: assembleStandardRelease
|
||||
wrapper-cache-enabled: true
|
||||
distributions-cache-enabled: true
|
||||
dependencies-cache-enabled: true
|
||||
configuration-cache-enabled: true
|
||||
|
||||
@@ -55,13 +45,10 @@ jobs:
|
||||
|
||||
- name: Get tag name
|
||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
||||
id: get_tag_name
|
||||
run: |
|
||||
set -x
|
||||
echo "VERSION_TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
|
||||
|
||||
# TODO: need to support multiple APKs
|
||||
|
||||
- name: Sign APK
|
||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
@@ -75,9 +62,23 @@ jobs:
|
||||
- name: Clean up build artifacts
|
||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
||||
run: |
|
||||
cp ${{ env.SIGNED_RELEASE_FILE }} tachiyomi-${{ env.VERSION_TAG }}.apk
|
||||
md5=`md5sum tachiyomi-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
|
||||
echo "APK_MD5=$md5" >> $GITHUB_ENV
|
||||
set -e
|
||||
|
||||
mv app/build/outputs/apk/standard/release/app-standard-universal-release-unsigned-signed.apk tachiyomi-${{ env.VERSION_TAG }}.apk
|
||||
sha=`sha256sum tachiyomi-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
|
||||
echo "APK_UNIVERSAL_SHA=$sha" >> $GITHUB_ENV
|
||||
|
||||
cp app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned-signed.apk tachiyomi-arm64-v8a-${{ env.VERSION_TAG }}.apk
|
||||
sha=`sha256sum tachiyomi-arm64-v8a-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
|
||||
echo "APK_ARM64_V8A_SHA=$sha" >> $GITHUB_ENV
|
||||
|
||||
cp app/build/outputs/apk/standard/release/app-standard-armeabi-v7a-release-unsigned-signed.apk tachiyomi-armeabi-v7a-${{ env.VERSION_TAG }}.apk
|
||||
sha=`sha256sum tachiyomi-armeabi-v7a-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
|
||||
echo "APK_ARMEABI_V7A_SHA=$sha" >> $GITHUB_ENV
|
||||
|
||||
cp app/build/outputs/apk/standard/release/app-standard-x86-release-unsigned-signed.apk tachiyomi-x86-${{ env.VERSION_TAG }}.apk
|
||||
sha=`sha256sum tachiyomi-x86-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
|
||||
echo "APK_X86_SHA=$sha" >> $GITHUB_ENV
|
||||
|
||||
- name: Create Release
|
||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
||||
@@ -86,9 +87,21 @@ jobs:
|
||||
tag_name: ${{ env.VERSION_TAG }}
|
||||
name: Tachiyomi ${{ env.VERSION_TAG }}
|
||||
body: |
|
||||
MD5: ${{ env.APK_MD5 }}
|
||||
---
|
||||
|
||||
### Checksums
|
||||
|
||||
| Variant | SHA-256 |
|
||||
| ------- | ------- |
|
||||
| Universal | ${{ env.APK_UNIVERSAL_SHA }}
|
||||
| arm64-v8a | ${{ env.APK_ARM64_V8A_SHA }}
|
||||
| armeabi-v7a | ${{ env.APK_ARMEABI_V7A_SHA }}
|
||||
| x86 | ${{ env.APK_X86_SHA }} |
|
||||
files: |
|
||||
tachiyomi-${{ env.VERSION_TAG }}.apk
|
||||
tachiyomi-arm64-v8a-${{ env.VERSION_TAG }}.apk
|
||||
tachiyomi-armeabi-v7a-${{ env.VERSION_TAG }}.apk
|
||||
tachiyomi-x86-${{ env.VERSION_TAG }}.apk
|
||||
draft: true
|
||||
prerelease: false
|
||||
env:
|
15
.github/workflows/cancel_pull_request.yml
vendored
Normal file
15
.github/workflows/cancel_pull_request.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
name: Cancel old pull request workflows
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["PR build check"]
|
||||
types:
|
||||
- requested
|
||||
|
||||
jobs:
|
||||
cancel:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: styfle/cancel-workflow-action@0.8.0
|
||||
with:
|
||||
workflow_id: ${{ github.event.workflow.id }}
|
@@ -1,76 +1,126 @@
|
||||
# Code of Conduct
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
Community moderators are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
Community moderators have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at the Tachiyomi [Discord server](https://discord.gg/tachiyomi). All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
reported to the community moderators responsible for enforcement at
|
||||
the [Tachiyomi Discord server](https://discord.gg/tachiyomi).
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
All community moderators are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community moderators will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community moderators, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/),
|
||||
version 2.1, available at
|
||||
[v2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[FAQ](https://www.contributor-covenant.org/faq). Translations are available
|
||||
at [translations](https://www.contributor-covenant.org/translations).
|
||||
|
@@ -6,8 +6,6 @@
|
||||
# Tachiyomi
|
||||
Tachiyomi is a free and open source manga reader for Android 6.0 and above.
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
Features include:
|
||||
@@ -38,7 +36,7 @@ Please make sure to read the full guidelines. Your issue may be closed without w
|
||||
|
||||
<details><summary>Bugs</summary>
|
||||
|
||||
* Include version (More > About > Version)
|
||||
* Include version (More → About → Version)
|
||||
* If not latest, try updating, it may have already been solved
|
||||
* Preview version is equal to the number of commits as seen in the main page
|
||||
* Include steps to reproduce (if not obvious from description)
|
||||
|
@@ -29,8 +29,8 @@ android {
|
||||
minSdk = AndroidConfig.minSdk
|
||||
targetSdk = AndroidConfig.targetSdk
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
versionCode = 66
|
||||
versionName = "0.12.0"
|
||||
versionCode = 68
|
||||
versionName = "0.12.2"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||
@@ -47,7 +47,7 @@ android {
|
||||
|
||||
splits {
|
||||
abi {
|
||||
isEnable = false
|
||||
isEnable = true
|
||||
reset()
|
||||
include(*SUPPORTED_ABIS.toTypedArray())
|
||||
isUniversalApk = true
|
||||
@@ -79,7 +79,7 @@ android {
|
||||
getByName("debugFull").res.srcDirs("src/debug/res")
|
||||
}
|
||||
|
||||
flavorDimensions("default")
|
||||
flavorDimensions.add("default")
|
||||
|
||||
productFlavors {
|
||||
create("standard") {
|
||||
@@ -87,18 +87,20 @@ android {
|
||||
dimension = "default"
|
||||
}
|
||||
create("dev") {
|
||||
resConfigs("en", "xxhdpi")
|
||||
resourceConfigurations.addAll(listOf("en", "xxhdpi"))
|
||||
dimension = "default"
|
||||
}
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude("META-INF/DEPENDENCIES")
|
||||
exclude("LICENSE.txt")
|
||||
exclude("META-INF/LICENSE")
|
||||
exclude("META-INF/LICENSE.txt")
|
||||
exclude("META-INF/NOTICE")
|
||||
exclude("META-INF/*.kotlin_module")
|
||||
resources.excludes.addAll(listOf(
|
||||
"META-INF/DEPENDENCIES",
|
||||
"LICENSE.txt",
|
||||
"META-INF/LICENSE",
|
||||
"META-INF/LICENSE.txt",
|
||||
"META-INF/NOTICE",
|
||||
"META-INF/*.kotlin_module",
|
||||
))
|
||||
}
|
||||
|
||||
dependenciesInfo {
|
||||
@@ -126,10 +128,9 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN))
|
||||
|
||||
val coroutinesVersion = "1.5.1"
|
||||
val coroutinesVersion = "1.5.2"
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
|
||||
|
||||
@@ -141,12 +142,10 @@ dependencies {
|
||||
implementation("androidx.appcompat:appcompat:1.4.0-alpha03")
|
||||
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha03")
|
||||
implementation("androidx.browser:browser:1.3.0")
|
||||
implementation("androidx.cardview:cardview:1.0.0")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.0")
|
||||
implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
|
||||
implementation("androidx.core:core-ktx:1.7.0-alpha01")
|
||||
implementation("androidx.core:core-ktx:1.7.0-alpha02")
|
||||
implementation("androidx.core:core-splashscreen:1.0.0-alpha01")
|
||||
implementation("androidx.preference:preference-ktx:1.1.1")
|
||||
implementation("androidx.recyclerview:recyclerview:1.2.1")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
||||
|
||||
@@ -156,18 +155,13 @@ dependencies {
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
|
||||
|
||||
// Job scheduling
|
||||
implementation("androidx.work:work-runtime-ktx:2.6.0-beta01")
|
||||
implementation("androidx.work:work-runtime-ktx:2.6.0")
|
||||
|
||||
// UI library
|
||||
implementation("com.google.android.material:material:1.5.0-alpha01")
|
||||
|
||||
"standardImplementation"("com.google.firebase:firebase-core:19.0.0")
|
||||
|
||||
// ReactiveX
|
||||
// RX
|
||||
implementation("io.reactivex:rxandroid:1.2.1")
|
||||
implementation("io.reactivex:rxjava:1.3.8")
|
||||
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
|
||||
implementation("com.github.pwittchen:reactivenetwork:0.13.0")
|
||||
implementation("ru.beryukhov:flowreactivenetwork:1.0.4")
|
||||
|
||||
// Network client
|
||||
val okhttpVersion = "4.9.1"
|
||||
@@ -179,24 +173,26 @@ dependencies {
|
||||
// TLS 1.3 support for Android < 10
|
||||
implementation("org.conscrypt:conscrypt-android:2.5.2")
|
||||
|
||||
// JSON
|
||||
val kotlinSerializationVersion = "1.2.2"
|
||||
// Data serialization (JSON, protobuf)
|
||||
val kotlinSerializationVersion = "1.3.0-RC"
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
|
||||
|
||||
// TODO: remove these once they're no longer used in any extensions
|
||||
implementation("com.google.code.gson:gson:2.8.7")
|
||||
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
|
||||
|
||||
// JavaScript engine
|
||||
implementation("com.squareup.duktape:duktape-android:1.3.0")
|
||||
implementation("com.squareup.duktape:duktape-android:1.4.0")
|
||||
|
||||
// HTML parser
|
||||
implementation("org.jsoup:jsoup:1.14.2")
|
||||
|
||||
// Disk
|
||||
implementation("com.jakewharton:disklrucache:2.0.2")
|
||||
implementation("com.github.tachiyomiorg:unifile:17bec43")
|
||||
implementation("com.github.junrar:junrar:7.4.0")
|
||||
|
||||
// HTML parser
|
||||
implementation("org.jsoup:jsoup:1.14.1")
|
||||
|
||||
// Database
|
||||
implementation("androidx.sqlite:sqlite-ktx:2.1.0")
|
||||
implementation("com.github.inorichi.storio:storio-common:8be19de@aar")
|
||||
@@ -204,6 +200,7 @@ dependencies {
|
||||
implementation("com.github.requery:sqlite-android:3.36.0")
|
||||
|
||||
// Preferences
|
||||
implementation("androidx.preference:preference-ktx:1.1.1")
|
||||
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.4.0")
|
||||
|
||||
// Model View Presenter
|
||||
@@ -214,7 +211,7 @@ dependencies {
|
||||
// Dependency injection
|
||||
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
||||
|
||||
// Image library
|
||||
// Image loading
|
||||
val coilVersion = "1.3.2"
|
||||
implementation("io.coil-kt:coil:$coilVersion")
|
||||
implementation("io.coil-kt:coil-gif:$coilVersion")
|
||||
@@ -224,16 +221,11 @@ dependencies {
|
||||
}
|
||||
implementation("com.github.tachiyomiorg:image-decoder:7481a4a")
|
||||
|
||||
// Logging
|
||||
implementation("com.jakewharton.timber:timber:4.7.1")
|
||||
|
||||
// Crash reports
|
||||
implementation("ch.acra:acra-http:5.8.1")
|
||||
|
||||
// Sort
|
||||
implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1")
|
||||
|
||||
// UI
|
||||
// UI libraries
|
||||
implementation("com.google.android.material:material:1.5.0-alpha03")
|
||||
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")
|
||||
@@ -256,8 +248,15 @@ dependencies {
|
||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-swiperefreshlayout:$flowbindingVersion")
|
||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbindingVersion")
|
||||
|
||||
// Logging
|
||||
implementation("com.jakewharton.timber:timber:5.0.1")
|
||||
|
||||
// Crash reports/analytics
|
||||
implementation("ch.acra:acra-http:5.8.1")
|
||||
"standardImplementation"("com.google.firebase:firebase-analytics:19.0.1")
|
||||
|
||||
// Licenses
|
||||
implementation("com.mikepenz:aboutlibraries:${BuildPluginsVersion.ABOUTLIB_PLUGIN}")
|
||||
implementation("com.mikepenz:aboutlibraries-core:${BuildPluginsVersion.ABOUTLIB_PLUGIN}")
|
||||
|
||||
// Tests
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
|
@@ -198,6 +198,11 @@
|
||||
android:resource="@xml/provider_paths" />
|
||||
</provider>
|
||||
|
||||
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
|
||||
android:value="false" />
|
||||
<meta-data android:name="android.webkit.WebView.MetricsOptOut"
|
||||
android:value="true" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
@@ -30,6 +30,8 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
||||
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil
|
||||
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
||||
import eu.kanade.tachiyomi.util.system.notification
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
@@ -114,17 +116,17 @@ open class App : Application(), LifecycleObserver, ImageLoaderFactory {
|
||||
override fun newImageLoader(): ImageLoader {
|
||||
return ImageLoader.Builder(this).apply {
|
||||
componentRegistry {
|
||||
add(TachiyomiImageDecoder(this@App.resources))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
add(ImageDecoderDecoder(this@App))
|
||||
} else {
|
||||
add(GifDecoder())
|
||||
}
|
||||
add(TachiyomiImageDecoder(this@App.resources))
|
||||
add(ByteBufferFetcher())
|
||||
add(MangaCoverFetcher())
|
||||
}
|
||||
okHttpClient(Injekt.get<NetworkHelper>().coilClient)
|
||||
crossfade(300)
|
||||
crossfade((300 * this@App.animatorDurationScale).toInt())
|
||||
allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
|
||||
}.build()
|
||||
}
|
||||
@@ -132,7 +134,7 @@ open class App : Application(), LifecycleObserver, ImageLoaderFactory {
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
|
||||
@Suppress("unused")
|
||||
fun onAppBackgrounded() {
|
||||
if (preferences.lockAppAfter().get() >= 0) {
|
||||
if (!AuthenticatorUtil.isAuthenticating && preferences.lockAppAfter().get() >= 0) {
|
||||
SecureActivityDelegate.locked = true
|
||||
}
|
||||
}
|
||||
@@ -176,8 +178,6 @@ open class App : Application(), LifecycleObserver, ImageLoaderFactory {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE"
|
||||
}
|
||||
}
|
||||
|
||||
private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE"
|
||||
|
@@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.job.DelayedTrackingStore
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
@@ -23,6 +24,8 @@ class AppModule(val app: Application) : InjektModule {
|
||||
override fun InjektRegistrar.registerInjectables() {
|
||||
addSingleton(app)
|
||||
|
||||
addSingletonFactory { Json { ignoreUnknownKeys = true } }
|
||||
|
||||
addSingletonFactory { PreferencesHelper(app) }
|
||||
|
||||
addSingletonFactory { DatabaseHelper(app) }
|
||||
@@ -41,7 +44,7 @@ class AppModule(val app: Application) : InjektModule {
|
||||
|
||||
addSingletonFactory { TrackManager(app) }
|
||||
|
||||
addSingletonFactory { Json { ignoreUnknownKeys = true } }
|
||||
addSingletonFactory { DelayedTrackingStore(app) }
|
||||
|
||||
// Asynchronously init expensive components for a faster cold start
|
||||
ContextCompat.getMainExecutor(app).execute {
|
||||
|
@@ -32,23 +32,20 @@ object Migrations {
|
||||
fun upgrade(preferences: PreferencesHelper): Boolean {
|
||||
val context = preferences.context
|
||||
|
||||
// Cancel app updater job for debug builds that don't include it
|
||||
if (BuildConfig.DEBUG && !BuildConfig.INCLUDE_UPDATER) {
|
||||
UpdaterJob.cancelTask(context)
|
||||
}
|
||||
|
||||
val oldVersion = preferences.lastVersionCode().get()
|
||||
if (oldVersion < BuildConfig.VERSION_CODE) {
|
||||
preferences.lastVersionCode().set(BuildConfig.VERSION_CODE)
|
||||
|
||||
// Always set up background tasks to ensure they're running
|
||||
if (BuildConfig.INCLUDE_UPDATER) {
|
||||
UpdaterJob.setupTask(context)
|
||||
}
|
||||
ExtensionUpdateJob.setupTask(context)
|
||||
LibraryUpdateJob.setupTask(context)
|
||||
BackupCreatorJob.setupTask(context)
|
||||
|
||||
// Fresh install
|
||||
if (oldVersion == 0) {
|
||||
// Set up default background tasks
|
||||
if (BuildConfig.INCLUDE_UPDATER) {
|
||||
UpdaterJob.setupTask(context)
|
||||
}
|
||||
ExtensionUpdateJob.setupTask(context)
|
||||
LibraryUpdateJob.setupTask(context)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -232,11 +229,7 @@ object Migrations {
|
||||
putString(PreferenceKeys.librarySortingDirection, newSortingDirection.name)
|
||||
}
|
||||
}
|
||||
if (oldVersion < 65) {
|
||||
if (preferences.lang().get() in listOf("en-US", "en-GB")) {
|
||||
preferences.lang().set("en")
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
@@ -53,6 +53,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||
backup = Backup(
|
||||
backupManga(databaseManga, flags),
|
||||
backupCategories(),
|
||||
emptyList(),
|
||||
backupExtensionInfo(databaseManga)
|
||||
)
|
||||
}
|
||||
|
@@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.data.backup.full.models.BackupCategory
|
||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupHistory
|
||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupManga
|
||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
|
||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupSource
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
@@ -33,7 +34,8 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
|
||||
}
|
||||
|
||||
// Store source mapping for error messages
|
||||
sourceMapping = backup.backupSources.map { it.sourceId to it.name }.toMap()
|
||||
var backupMaps = backup.backupBrokenSources.map { BackupSource(it.name, it.sourceId) } + backup.backupSources
|
||||
sourceMapping = backupMaps.map { it.sourceId to it.name }.toMap()
|
||||
|
||||
// Restore individual manga
|
||||
backup.backupManga.forEach {
|
||||
@@ -62,7 +64,7 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
|
||||
val manga = backupManga.getMangaImpl()
|
||||
val chapters = backupManga.getChaptersImpl()
|
||||
val categories = backupManga.categories
|
||||
val history = backupManga.history
|
||||
val history = backupManga.brokenHistory.map { BackupHistory(it.url, it.lastRead) } + backupManga.history
|
||||
val tracks = backupManga.getTrackingImpl()
|
||||
|
||||
try {
|
||||
|
@@ -8,5 +8,6 @@ data class Backup(
|
||||
@ProtoNumber(1) val backupManga: List<BackupManga>,
|
||||
@ProtoNumber(2) var backupCategories: List<BackupCategory> = emptyList(),
|
||||
// Bump by 100 to specify this is a 0.x value
|
||||
@ProtoNumber(100) var backupSources: List<BackupSource> = emptyList(),
|
||||
@ProtoNumber(100) var backupBrokenSources: List<BrokenBackupSource> = emptyList(),
|
||||
@ProtoNumber(101) var backupSources: List<BackupSource> = emptyList()
|
||||
)
|
||||
|
@@ -4,7 +4,13 @@ import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.protobuf.ProtoNumber
|
||||
|
||||
@Serializable
|
||||
data class BackupHistory(
|
||||
data class BrokenBackupHistory(
|
||||
@ProtoNumber(0) var url: String,
|
||||
@ProtoNumber(1) var lastRead: Long
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class BackupHistory(
|
||||
@ProtoNumber(1) var url: String,
|
||||
@ProtoNumber(2) var lastRead: Long
|
||||
)
|
||||
|
@@ -33,8 +33,9 @@ data class BackupManga(
|
||||
// Bump by 100 for values that are not saved/implemented in 1.x but are used in 0.x
|
||||
@ProtoNumber(100) var favorite: Boolean = true,
|
||||
@ProtoNumber(101) var chapterFlags: Int = 0,
|
||||
@ProtoNumber(102) var history: List<BackupHistory> = emptyList(),
|
||||
@ProtoNumber(103) var viewer_flags: Int? = null
|
||||
@ProtoNumber(102) var brokenHistory: List<BrokenBackupHistory> = emptyList(),
|
||||
@ProtoNumber(103) var viewer_flags: Int? = null,
|
||||
@ProtoNumber(104) var history: List<BackupHistory> = emptyList()
|
||||
) {
|
||||
fun getMangaImpl(): MangaImpl {
|
||||
return MangaImpl().apply {
|
||||
|
@@ -5,9 +5,15 @@ import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.protobuf.ProtoNumber
|
||||
|
||||
@Serializable
|
||||
data class BackupSource(
|
||||
data class BrokenBackupSource(
|
||||
@ProtoNumber(0) var name: String = "",
|
||||
@ProtoNumber(1) var sourceId: Long
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class BackupSource(
|
||||
@ProtoNumber(1) var name: String = "",
|
||||
@ProtoNumber(2) var sourceId: Long
|
||||
) {
|
||||
companion object {
|
||||
fun copyFrom(source: Source): BackupSource {
|
||||
|
@@ -32,8 +32,7 @@ data class BackupTracking(
|
||||
media_id = this@BackupTracking.mediaId
|
||||
library_id = this@BackupTracking.libraryId
|
||||
title = this@BackupTracking.title
|
||||
// convert from float to int because of 1.x types
|
||||
last_chapter_read = this@BackupTracking.lastChapterRead.toInt()
|
||||
last_chapter_read = this@BackupTracking.lastChapterRead
|
||||
total_chapters = this@BackupTracking.totalChapters
|
||||
score = this@BackupTracking.score
|
||||
status = this@BackupTracking.status
|
||||
@@ -51,8 +50,7 @@ data class BackupTracking(
|
||||
// forced not null so its compatible with 1.x backup system
|
||||
libraryId = track.library_id!!,
|
||||
title = track.title,
|
||||
// convert to float for 1.x
|
||||
lastChapterRead = track.last_chapter_read.toFloat(),
|
||||
lastChapterRead = track.last_chapter_read,
|
||||
totalChapters = track.total_chapters,
|
||||
score = track.score,
|
||||
status = track.status,
|
||||
|
@@ -13,13 +13,12 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.decodeFromJsonElement
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import kotlinx.serialization.json.intOrNull
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import java.util.Date
|
||||
|
||||
@@ -28,8 +27,8 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
|
||||
override suspend fun performRestore(uri: Uri): Boolean {
|
||||
// Read the json and create a Json Object,
|
||||
// cannot use the backupManager json deserializer one because its not initialized yet
|
||||
val backupObject = Json.decodeFromString<JsonObject>(
|
||||
context.contentResolver.openInputStream(uri)!!.source().buffer().use { it.readUtf8() }
|
||||
val backupObject = Json.decodeFromStream<JsonObject>(
|
||||
context.contentResolver.openInputStream(uri)!!
|
||||
)
|
||||
|
||||
// Get parser version
|
||||
|
@@ -5,9 +5,7 @@ import android.net.Uri
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
|
||||
class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
||||
/**
|
||||
@@ -19,8 +17,8 @@ class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
||||
override fun validate(context: Context, uri: Uri): Results {
|
||||
val backupManager = LegacyBackupManager(context)
|
||||
|
||||
val backup = backupManager.parser.decodeFromString<Backup>(
|
||||
context.contentResolver.openInputStream(uri)!!.source().buffer().use { it.readUtf8() }
|
||||
val backup = backupManager.parser.decodeFromStream<Backup>(
|
||||
context.contentResolver.openInputStream(uri)!!
|
||||
)
|
||||
|
||||
if (backup.version == null) {
|
||||
|
@@ -10,6 +10,7 @@ import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.json.JsonDecoder
|
||||
import kotlinx.serialization.json.JsonEncoder
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.float
|
||||
import kotlinx.serialization.json.int
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
@@ -46,7 +47,7 @@ open class TrackBaseSerializer<T : Track> : KSerializer<T> {
|
||||
sync_id = jsonObject[SYNC]!!.jsonPrimitive.int
|
||||
media_id = jsonObject[MEDIA]!!.jsonPrimitive.int
|
||||
library_id = jsonObject[LIBRARY]!!.jsonPrimitive.long
|
||||
last_chapter_read = jsonObject[LAST_READ]!!.jsonPrimitive.int
|
||||
last_chapter_read = jsonObject[LAST_READ]!!.jsonPrimitive.float
|
||||
tracking_url = jsonObject[TRACKING_URL]!!.jsonPrimitive.content
|
||||
} as T
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@ import coil.network.HttpException
|
||||
import coil.request.get
|
||||
import coil.size.Size
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher.Companion.USE_CUSTOM_COVER
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
|
@@ -20,7 +20,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
||||
/**
|
||||
* Version of the database.
|
||||
*/
|
||||
const val DATABASE_VERSION = 12
|
||||
const val DATABASE_VERSION = 13
|
||||
}
|
||||
|
||||
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
||||
@@ -85,6 +85,12 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
||||
if (oldVersion < 12) {
|
||||
db.execSQL(MangaTable.addNextUpdateCol)
|
||||
}
|
||||
if (oldVersion < 13) {
|
||||
db.execSQL(TrackTable.renameTableToTemp)
|
||||
db.execSQL(TrackTable.createTableQuery)
|
||||
db.execSQL(TrackTable.insertFromTempTable)
|
||||
db.execSQL(TrackTable.dropTempTable)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConfigure(db: SupportSQLiteDatabase) {
|
||||
|
@@ -71,7 +71,7 @@ class TrackGetResolver : DefaultGetResolver<Track>() {
|
||||
media_id = cursor.getInt(cursor.getColumnIndex(COL_MEDIA_ID))
|
||||
library_id = cursor.getLong(cursor.getColumnIndex(COL_LIBRARY_ID))
|
||||
title = cursor.getString(cursor.getColumnIndex(COL_TITLE))
|
||||
last_chapter_read = cursor.getInt(cursor.getColumnIndex(COL_LAST_CHAPTER_READ))
|
||||
last_chapter_read = cursor.getFloat(cursor.getColumnIndex(COL_LAST_CHAPTER_READ))
|
||||
total_chapters = cursor.getInt(cursor.getColumnIndex(COL_TOTAL_CHAPTERS))
|
||||
status = cursor.getInt(cursor.getColumnIndex(COL_STATUS))
|
||||
score = cursor.getFloat(cursor.getColumnIndex(COL_SCORE))
|
||||
|
@@ -1,5 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
|
||||
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
|
||||
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
||||
@@ -37,6 +39,6 @@ interface Category : Serializable {
|
||||
this.name = name
|
||||
}
|
||||
|
||||
fun createDefault(): Category = create("Default").apply { id = 0 }
|
||||
fun createDefault(context: Context): Category = create(context.getString(R.string.label_default)).apply { id = 0 }
|
||||
}
|
||||
}
|
||||
|
@@ -16,7 +16,7 @@ interface Track : Serializable {
|
||||
|
||||
var title: String
|
||||
|
||||
var last_chapter_read: Int
|
||||
var last_chapter_read: Float
|
||||
|
||||
var total_chapters: Int
|
||||
|
||||
|
@@ -14,7 +14,7 @@ class TrackImpl : Track {
|
||||
|
||||
override lateinit var title: String
|
||||
|
||||
override var last_chapter_read: Int = 0
|
||||
override var last_chapter_read: Float = 0F
|
||||
|
||||
override var total_chapters: Int = 0
|
||||
|
||||
|
@@ -7,7 +7,13 @@ import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.*
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaCoverLastModifiedPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaNextUpdatedPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver
|
||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
||||
@@ -75,7 +81,7 @@ interface MangaQueries : DbProvider {
|
||||
|
||||
fun updateChapterFlags(manga: List<Manga>) = db.put()
|
||||
.objects(manga)
|
||||
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_CHAPTER_FLAGS, Manga::chapter_flags, true))
|
||||
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_CHAPTER_FLAGS, Manga::chapter_flags))
|
||||
.prepare()
|
||||
|
||||
fun updateViewerFlags(manga: Manga) = db.put()
|
||||
@@ -85,7 +91,7 @@ interface MangaQueries : DbProvider {
|
||||
|
||||
fun updateViewerFlags(manga: List<Manga>) = db.put()
|
||||
.objects(manga)
|
||||
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags, true))
|
||||
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags))
|
||||
.prepare()
|
||||
|
||||
fun updateNextUpdated(manga: Manga) = db.put()
|
||||
|
@@ -27,9 +27,7 @@ class HistoryLastReadPutResolver : HistoryPutResolver() {
|
||||
.build()
|
||||
)
|
||||
|
||||
val putResult: PutResult
|
||||
|
||||
putResult = cursor.use { putCursor ->
|
||||
cursor.use { putCursor ->
|
||||
if (putCursor.count == 0) {
|
||||
val insertQuery = mapToInsertQuery(history)
|
||||
val insertedId = db.lowLevel().insert(insertQuery, mapToContentValues(history))
|
||||
@@ -39,25 +37,15 @@ class HistoryLastReadPutResolver : HistoryPutResolver() {
|
||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||
}
|
||||
}
|
||||
|
||||
putResult
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates update query
|
||||
* @param obj history object
|
||||
*/
|
||||
override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder()
|
||||
.table(HistoryTable.TABLE)
|
||||
.where("${HistoryTable.COL_CHAPTER_ID} = ?")
|
||||
.whereArgs(obj.chapter_id)
|
||||
.build()
|
||||
|
||||
/**
|
||||
* Create content query
|
||||
* @param history object
|
||||
*/
|
||||
fun mapToUpdateContentValues(history: History) =
|
||||
private fun mapToUpdateContentValues(history: History) =
|
||||
contentValuesOf(
|
||||
HistoryTable.COL_LAST_READ to history.last_read
|
||||
)
|
||||
|
@@ -10,7 +10,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||
import kotlin.reflect.KProperty1
|
||||
|
||||
class MangaFlagsPutResolver(private val colName: String, private val fieldGetter: KProperty1<Manga, Int>, private val updateAll: Boolean = false) : PutResolver<Manga>() {
|
||||
class MangaFlagsPutResolver(private val colName: String, private val fieldGetter: KProperty1<Manga, Int>) : PutResolver<Manga>() {
|
||||
|
||||
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||
val updateQuery = mapToUpdateQuery(manga)
|
||||
@@ -20,21 +20,11 @@ class MangaFlagsPutResolver(private val colName: String, private val fieldGetter
|
||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||
}
|
||||
|
||||
fun mapToUpdateQuery(manga: Manga): UpdateQuery {
|
||||
val builder = UpdateQuery.builder()
|
||||
|
||||
return if (updateAll) {
|
||||
builder
|
||||
.table(MangaTable.TABLE)
|
||||
.build()
|
||||
} else {
|
||||
builder
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(manga: Manga) =
|
||||
contentValuesOf(
|
||||
|
@@ -1,6 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.content.ContentValues
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||
@@ -25,7 +25,7 @@ class MangaNextUpdatedPutResolver : PutResolver<Manga>() {
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||
put(MangaTable.COL_NEXT_UPDATE, manga.next_update)
|
||||
}
|
||||
fun mapToContentValues(manga: Manga) = contentValuesOf(
|
||||
MangaTable.COL_NEXT_UPDATE to manga.next_update
|
||||
)
|
||||
}
|
||||
|
@@ -39,7 +39,7 @@ object TrackTable {
|
||||
$COL_MEDIA_ID INTEGER NOT NULL,
|
||||
$COL_LIBRARY_ID INTEGER,
|
||||
$COL_TITLE TEXT NOT NULL,
|
||||
$COL_LAST_CHAPTER_READ INTEGER NOT NULL,
|
||||
$COL_LAST_CHAPTER_READ REAL NOT NULL,
|
||||
$COL_TOTAL_CHAPTERS INTEGER NOT NULL,
|
||||
$COL_STATUS INTEGER NOT NULL,
|
||||
$COL_SCORE FLOAT NOT NULL,
|
||||
@@ -62,4 +62,19 @@ object TrackTable {
|
||||
|
||||
val addFinishDate: String
|
||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_FINISH_DATE LONG NOT NULL DEFAULT 0"
|
||||
|
||||
val renameTableToTemp: String
|
||||
get() =
|
||||
"ALTER TABLE $TABLE RENAME TO ${TABLE}_tmp"
|
||||
|
||||
val insertFromTempTable: String
|
||||
get() =
|
||||
"""
|
||||
|INSERT INTO $TABLE($COL_ID,$COL_MANGA_ID,$COL_SYNC_ID,$COL_MEDIA_ID,$COL_LIBRARY_ID,$COL_TITLE,$COL_LAST_CHAPTER_READ,$COL_TOTAL_CHAPTERS,$COL_STATUS,$COL_SCORE,$COL_TRACKING_URL,$COL_START_DATE,$COL_FINISH_DATE)
|
||||
|SELECT $COL_ID,$COL_MANGA_ID,$COL_SYNC_ID,$COL_MEDIA_ID,$COL_LIBRARY_ID,$COL_TITLE,$COL_LAST_CHAPTER_READ,$COL_TOTAL_CHAPTERS,$COL_STATUS,$COL_SCORE,$COL_TRACKING_URL,$COL_START_DATE,$COL_FINISH_DATE
|
||||
|FROM ${TABLE}_tmp
|
||||
""".trimMargin()
|
||||
|
||||
val dropTempTable: String
|
||||
get() = "DROP TABLE ${TABLE}_tmp"
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
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.download.model.Download
|
||||
@@ -15,6 +16,8 @@ import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import rx.Observable
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
@@ -24,7 +27,10 @@ import uy.kohesive.injekt.injectLazy
|
||||
*
|
||||
* @param context the application context.
|
||||
*/
|
||||
class DownloadManager(private val context: Context) {
|
||||
class DownloadManager(
|
||||
private val context: Context,
|
||||
private val db: DatabaseHelper = Injekt.get()
|
||||
) {
|
||||
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
@@ -217,7 +223,7 @@ class DownloadManager(private val context: Context) {
|
||||
* @param download the download to cancel.
|
||||
*/
|
||||
fun deletePendingDownload(download: Download) {
|
||||
deleteChapters(listOf(download.chapter), download.manga, download.source)
|
||||
deleteChapters(listOf(download.chapter), download.manga, download.source, true)
|
||||
}
|
||||
|
||||
fun deletePendingDownloads(vararg downloads: Download) {
|
||||
@@ -225,7 +231,7 @@ class DownloadManager(private val context: Context) {
|
||||
downloadsByManga.map { entry ->
|
||||
val manga = entry.value.first().manga
|
||||
val source = entry.value.first().source
|
||||
deleteChapters(entry.value.map { it.chapter }, manga, source)
|
||||
deleteChapters(entry.value.map { it.chapter }, manga, source, true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,9 +241,15 @@ class DownloadManager(private val context: Context) {
|
||||
* @param chapters the list of chapters to delete.
|
||||
* @param manga the manga of the chapters.
|
||||
* @param source the source of the chapters.
|
||||
* @param isCancelling true if it's simply cancelling a download
|
||||
*/
|
||||
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source): List<Chapter> {
|
||||
val filteredChapters = getChaptersToDelete(chapters)
|
||||
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source, isCancelling: Boolean = false): List<Chapter> {
|
||||
val filteredChapters = if (isCancelling) {
|
||||
chapters
|
||||
} else {
|
||||
getChaptersToDelete(chapters, manga)
|
||||
}
|
||||
|
||||
launchIO {
|
||||
removeFromDownloadQueue(filteredChapters)
|
||||
|
||||
@@ -290,7 +302,7 @@ class DownloadManager(private val context: Context) {
|
||||
* @param manga the manga of the chapters.
|
||||
*/
|
||||
fun enqueueDeleteChapters(chapters: List<Chapter>, manga: Manga) {
|
||||
pendingDeleter.addChapters(getChaptersToDelete(chapters), manga)
|
||||
pendingDeleter.addChapters(getChaptersToDelete(chapters, manga), manga)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -330,8 +342,17 @@ class DownloadManager(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getChaptersToDelete(chapters: List<Chapter>): List<Chapter> {
|
||||
return if (!preferences.removeBookmarkedChapters()) {
|
||||
private fun getChaptersToDelete(chapters: List<Chapter>, manga: Manga): List<Chapter> {
|
||||
// Retrieve the categories that are set to exclude from being deleted on read
|
||||
val categoriesToExclude = preferences.removeExcludeCategories().get().map(String::toInt)
|
||||
val categoriesForManga = db.getCategoriesForManga(manga).executeAsBlocking()
|
||||
.mapNotNull { it.id }
|
||||
.takeUnless { it.isEmpty() }
|
||||
?: listOf(0)
|
||||
|
||||
return if (categoriesForManga.intersect(categoriesToExclude).isNotEmpty()) {
|
||||
chapters.filterNot { it.read }
|
||||
} else if (!preferences.removeBookmarkedChapters()) {
|
||||
chapters.filterNot { it.bookmark }
|
||||
} else {
|
||||
chapters
|
||||
|
@@ -52,7 +52,7 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
/**
|
||||
* Updated when error is thrown
|
||||
*/
|
||||
var errorThrown = false
|
||||
private var errorThrown = false
|
||||
|
||||
/**
|
||||
* Updated when paused
|
||||
|
@@ -4,26 +4,30 @@ import android.app.Notification
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkInfo.State.CONNECTED
|
||||
import android.net.NetworkInfo.State.DISCONNECTED
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.github.pwittchen.reactivenetwork.library.Connectivity
|
||||
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.util.lang.plusAssign
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||
import eu.kanade.tachiyomi.util.system.connectivityManager
|
||||
import eu.kanade.tachiyomi.util.system.isOnline
|
||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||
import eu.kanade.tachiyomi.util.system.notification
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import eu.kanade.tachiyomi.util.system.wifiManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import ru.beryukhov.reactivenetwork.ReactiveNetwork
|
||||
import rx.subscriptions.CompositeSubscription
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
@@ -80,16 +84,15 @@ class DownloadService : Service() {
|
||||
*/
|
||||
private lateinit var wakeLock: PowerManager.WakeLock
|
||||
|
||||
/**
|
||||
* Subscriptions to store while the service is running.
|
||||
*/
|
||||
private lateinit var subscriptions: CompositeSubscription
|
||||
private lateinit var ioScope: CoroutineScope
|
||||
|
||||
/**
|
||||
* Called when the service is created.
|
||||
*/
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
startForeground(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS, getPlaceholderNotification())
|
||||
wakeLock = acquireWakeLock(javaClass.name)
|
||||
runningRelay.call(true)
|
||||
@@ -102,6 +105,7 @@ class DownloadService : Service() {
|
||||
* Called when the service is destroyed.
|
||||
*/
|
||||
override fun onDestroy() {
|
||||
ioScope?.cancel()
|
||||
runningRelay.call(false)
|
||||
subscriptions.unsubscribe()
|
||||
downloadManager.stopDownloads()
|
||||
@@ -129,44 +133,42 @@ class DownloadService : Service() {
|
||||
* @see onNetworkStateChanged
|
||||
*/
|
||||
private fun listenNetworkChanges() {
|
||||
subscriptions += ReactiveNetwork.observeNetworkConnectivity(applicationContext)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ state ->
|
||||
onNetworkStateChanged(state)
|
||||
},
|
||||
{
|
||||
ReactiveNetwork()
|
||||
.observeNetworkConnectivity(applicationContext)
|
||||
.onEach {
|
||||
withUIContext {
|
||||
onNetworkStateChanged()
|
||||
}
|
||||
}
|
||||
.catch {
|
||||
withUIContext {
|
||||
toast(R.string.download_queue_error)
|
||||
stopSelf()
|
||||
}
|
||||
)
|
||||
}
|
||||
.launchIn(ioScope)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the network state changes.
|
||||
*
|
||||
* @param connectivity the new network state.
|
||||
*/
|
||||
private fun onNetworkStateChanged(connectivity: Connectivity) {
|
||||
when (connectivity.state) {
|
||||
CONNECTED -> {
|
||||
if (preferences.downloadOnlyOverWifi() && connectivityManager.activeNetworkInfo?.type != ConnectivityManager.TYPE_WIFI) {
|
||||
downloadManager.stopDownloads(getString(R.string.download_notifier_text_only_wifi))
|
||||
} else {
|
||||
val started = downloadManager.startDownloads()
|
||||
if (!started) stopSelf()
|
||||
}
|
||||
}
|
||||
DISCONNECTED -> {
|
||||
downloadManager.stopDownloads(getString(R.string.download_notifier_no_network))
|
||||
}
|
||||
else -> {
|
||||
/* Do nothing */
|
||||
private fun onNetworkStateChanged() {
|
||||
if (isOnline()) {
|
||||
if (preferences.downloadOnlyOverWifi() && !wifiManager.isWifiEnabled) {
|
||||
stopDownloads(R.string.download_notifier_text_only_wifi)
|
||||
} else {
|
||||
val started = downloadManager.startDownloads()
|
||||
if (!started) stopSelf()
|
||||
}
|
||||
} else {
|
||||
stopDownloads(R.string.download_notifier_no_network)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopDownloads(@StringRes string: Int) {
|
||||
downloadManager.stopDownloads(getString(string))
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens to downloader status. Enables or disables the wake lock depending on the status.
|
||||
*/
|
||||
|
@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.data.library
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import java.util.Collections
|
||||
import kotlin.Comparator
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
|
@@ -294,48 +294,46 @@ class LibraryUpdateService(
|
||||
return@async
|
||||
}
|
||||
|
||||
currentlyUpdatingManga.add(manga)
|
||||
notifier.showProgressNotification(
|
||||
withUpdateNotification(
|
||||
currentlyUpdatingManga,
|
||||
progressCount.get(),
|
||||
mangaToUpdate.size
|
||||
)
|
||||
progressCount,
|
||||
manga,
|
||||
) { manga ->
|
||||
try {
|
||||
val (newChapters, _) = updateManga(manga)
|
||||
|
||||
try {
|
||||
val (newChapters, _) = updateManga(manga)
|
||||
if (newChapters.isNotEmpty()) {
|
||||
if (manga.shouldDownloadNewChapters(db, preferences)) {
|
||||
downloadChapters(manga, newChapters)
|
||||
hasDownloads.set(true)
|
||||
}
|
||||
|
||||
if (newChapters.isNotEmpty()) {
|
||||
if (manga.shouldDownloadNewChapters(db, preferences)) {
|
||||
downloadChapters(manga, newChapters)
|
||||
hasDownloads.set(true)
|
||||
// Convert to the manga that contains new chapters
|
||||
newUpdates.add(
|
||||
manga to newChapters.sortedByDescending { ch -> ch.source_order }
|
||||
.toTypedArray()
|
||||
)
|
||||
}
|
||||
|
||||
// Convert to the manga that contains new chapters
|
||||
newUpdates.add(manga to newChapters.sortedByDescending { ch -> ch.source_order }.toTypedArray())
|
||||
} catch (e: Throwable) {
|
||||
val errorMessage = when (e) {
|
||||
is NoChaptersException -> {
|
||||
getString(R.string.no_chapters_error)
|
||||
}
|
||||
is SourceManager.SourceNotInstalledException -> {
|
||||
// failedUpdates will already have the source, don't need to copy it into the message
|
||||
getString(R.string.loader_not_implemented_error)
|
||||
}
|
||||
else -> {
|
||||
e.message
|
||||
}
|
||||
}
|
||||
failedUpdates.add(manga to errorMessage)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
val errorMessage = if (e is NoChaptersException) {
|
||||
getString(R.string.no_chapters_error)
|
||||
} else if (e is SourceManager.SourceNotInstalledException) {
|
||||
// failedUpdates will already have the source, don't need to copy it into the message
|
||||
getString(R.string.loader_not_implemented_error)
|
||||
} else {
|
||||
e.message
|
||||
|
||||
if (preferences.autoUpdateTrackers()) {
|
||||
updateTrackings(manga, loggedServices)
|
||||
}
|
||||
failedUpdates.add(manga to errorMessage)
|
||||
}
|
||||
|
||||
if (preferences.autoUpdateTrackers()) {
|
||||
updateTrackings(manga, loggedServices)
|
||||
}
|
||||
|
||||
currentlyUpdatingManga.remove(manga)
|
||||
progressCount.andIncrement
|
||||
notifier.showProgressNotification(
|
||||
currentlyUpdatingManga,
|
||||
progressCount.get(),
|
||||
mangaToUpdate.size
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -418,36 +416,35 @@ class LibraryUpdateService(
|
||||
return@async
|
||||
}
|
||||
|
||||
currentlyUpdatingManga.add(manga)
|
||||
notifier.showProgressNotification(
|
||||
withUpdateNotification(
|
||||
currentlyUpdatingManga,
|
||||
progressCount.get(),
|
||||
mangaToUpdate.size
|
||||
)
|
||||
|
||||
sourceManager.get(manga.source)?.let { source ->
|
||||
try {
|
||||
val networkManga =
|
||||
source.getMangaDetails(manga.toMangaInfo())
|
||||
val sManga = networkManga.toSManga()
|
||||
manga.prepUpdateCover(coverCache, sManga, true)
|
||||
sManga.thumbnail_url?.let {
|
||||
manga.thumbnail_url = it
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
progressCount,
|
||||
manga,
|
||||
) { manga ->
|
||||
sourceManager.get(manga.source)?.let { source ->
|
||||
try {
|
||||
val networkManga =
|
||||
source.getMangaDetails(manga.toMangaInfo())
|
||||
val sManga = networkManga.toSManga()
|
||||
manga.prepUpdateCover(coverCache, sManga, true)
|
||||
sManga.thumbnail_url?.let {
|
||||
manga.thumbnail_url = it
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
// Ignore errors and continue
|
||||
Timber.e(e)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
// Ignore errors and continue
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
|
||||
currentlyUpdatingManga.remove(manga)
|
||||
progressCount.andIncrement
|
||||
notifier.showProgressNotification(
|
||||
currentlyUpdatingManga,
|
||||
progressCount.get(),
|
||||
mangaToUpdate.size
|
||||
)
|
||||
currentlyUpdatingManga.remove(manga)
|
||||
progressCount.andIncrement
|
||||
notifier.showProgressNotification(
|
||||
currentlyUpdatingManga,
|
||||
progressCount.get(),
|
||||
mangaToUpdate.size
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -506,6 +503,38 @@ class LibraryUpdateService(
|
||||
.awaitAll()
|
||||
}
|
||||
|
||||
private suspend fun withUpdateNotification(
|
||||
updatingManga: CopyOnWriteArrayList<LibraryManga>,
|
||||
completed: AtomicInteger,
|
||||
manga: LibraryManga,
|
||||
block: suspend (LibraryManga) -> Unit,
|
||||
) {
|
||||
if (updateJob?.isActive != true) {
|
||||
return
|
||||
}
|
||||
|
||||
updatingManga.add(manga)
|
||||
notifier.showProgressNotification(
|
||||
updatingManga,
|
||||
completed.get(),
|
||||
mangaToUpdate.size
|
||||
)
|
||||
|
||||
block(manga)
|
||||
|
||||
if (updateJob?.isActive != true) {
|
||||
return
|
||||
}
|
||||
|
||||
updatingManga.remove(manga)
|
||||
completed.andIncrement
|
||||
notifier.showProgressNotification(
|
||||
updatingManga,
|
||||
completed.get(),
|
||||
mangaToUpdate.size
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes basic file of update errors to cache dir.
|
||||
*/
|
||||
|
@@ -1,12 +1,13 @@
|
||||
package eu.kanade.tachiyomi.data.notification
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationChannelGroup
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.NotificationManagerCompat.IMPORTANCE_DEFAULT
|
||||
import androidx.core.app.NotificationManagerCompat.IMPORTANCE_HIGH
|
||||
import androidx.core.app.NotificationManagerCompat.IMPORTANCE_LOW
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||
import eu.kanade.tachiyomi.util.system.buildNotificationChannel
|
||||
import eu.kanade.tachiyomi.util.system.buildNotificationChannelGroup
|
||||
|
||||
/**
|
||||
* Class to manage the basic information of all the notifications used in the app.
|
||||
@@ -81,94 +82,73 @@ object Notifications {
|
||||
|
||||
/**
|
||||
* Creates the notification channels introduced in Android Oreo.
|
||||
* This won't do anything on Android versions that don't support notification channels.
|
||||
*
|
||||
* @param context The application context.
|
||||
*/
|
||||
fun createChannels(context: Context) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
val notificationService = NotificationManagerCompat.from(context)
|
||||
|
||||
listOf(
|
||||
NotificationChannelGroup(GROUP_BACKUP_RESTORE, context.getString(R.string.group_backup_restore)),
|
||||
NotificationChannelGroup(GROUP_DOWNLOADER, context.getString(R.string.group_downloader))
|
||||
).forEach(context.notificationManager::createNotificationChannelGroup)
|
||||
val channelGroupList = listOf(
|
||||
buildNotificationChannelGroup(GROUP_BACKUP_RESTORE) {
|
||||
setName(context.getString(R.string.group_backup_restore))
|
||||
},
|
||||
buildNotificationChannelGroup(GROUP_DOWNLOADER) {
|
||||
setName(context.getString(R.string.group_downloader))
|
||||
}
|
||||
)
|
||||
notificationService.createNotificationChannelGroupsCompat(channelGroupList)
|
||||
|
||||
listOf(
|
||||
NotificationChannel(
|
||||
CHANNEL_COMMON,
|
||||
context.getString(R.string.channel_common),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
),
|
||||
NotificationChannel(
|
||||
CHANNEL_LIBRARY,
|
||||
context.getString(R.string.channel_library),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
val channelList = listOf(
|
||||
buildNotificationChannel(CHANNEL_COMMON, IMPORTANCE_LOW) {
|
||||
setName(context.getString(R.string.channel_common))
|
||||
},
|
||||
buildNotificationChannel(CHANNEL_LIBRARY, IMPORTANCE_LOW) {
|
||||
setName(context.getString(R.string.channel_library))
|
||||
setShowBadge(false)
|
||||
},
|
||||
NotificationChannel(
|
||||
CHANNEL_DOWNLOADER_PROGRESS,
|
||||
context.getString(R.string.channel_progress),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
group = GROUP_DOWNLOADER
|
||||
buildNotificationChannel(CHANNEL_DOWNLOADER_PROGRESS, IMPORTANCE_LOW) {
|
||||
setName(context.getString(R.string.channel_progress))
|
||||
setGroup(GROUP_DOWNLOADER)
|
||||
setShowBadge(false)
|
||||
},
|
||||
NotificationChannel(
|
||||
CHANNEL_DOWNLOADER_COMPLETE,
|
||||
context.getString(R.string.channel_complete),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
group = GROUP_DOWNLOADER
|
||||
buildNotificationChannel(CHANNEL_DOWNLOADER_COMPLETE, IMPORTANCE_LOW) {
|
||||
setName(context.getString(R.string.channel_complete))
|
||||
setGroup(GROUP_DOWNLOADER)
|
||||
setShowBadge(false)
|
||||
},
|
||||
NotificationChannel(
|
||||
CHANNEL_DOWNLOADER_ERROR,
|
||||
context.getString(R.string.channel_errors),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
group = GROUP_DOWNLOADER
|
||||
buildNotificationChannel(CHANNEL_DOWNLOADER_ERROR, IMPORTANCE_LOW) {
|
||||
setName(context.getString(R.string.channel_errors))
|
||||
setGroup(GROUP_DOWNLOADER)
|
||||
setShowBadge(false)
|
||||
},
|
||||
NotificationChannel(
|
||||
CHANNEL_NEW_CHAPTERS,
|
||||
context.getString(R.string.channel_new_chapters),
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
),
|
||||
NotificationChannel(
|
||||
CHANNEL_UPDATES_TO_EXTS,
|
||||
context.getString(R.string.channel_ext_updates),
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
),
|
||||
NotificationChannel(
|
||||
CHANNEL_BACKUP_RESTORE_PROGRESS,
|
||||
context.getString(R.string.channel_progress),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
group = GROUP_BACKUP_RESTORE
|
||||
buildNotificationChannel(CHANNEL_NEW_CHAPTERS, IMPORTANCE_DEFAULT) {
|
||||
setName(context.getString(R.string.channel_new_chapters))
|
||||
},
|
||||
buildNotificationChannel(CHANNEL_UPDATES_TO_EXTS, IMPORTANCE_DEFAULT) {
|
||||
setName(context.getString(R.string.channel_ext_updates))
|
||||
},
|
||||
buildNotificationChannel(CHANNEL_BACKUP_RESTORE_PROGRESS, IMPORTANCE_LOW) {
|
||||
setName(context.getString(R.string.channel_progress))
|
||||
setGroup(GROUP_BACKUP_RESTORE)
|
||||
setShowBadge(false)
|
||||
},
|
||||
NotificationChannel(
|
||||
CHANNEL_BACKUP_RESTORE_COMPLETE,
|
||||
context.getString(R.string.channel_complete),
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
).apply {
|
||||
group = GROUP_BACKUP_RESTORE
|
||||
buildNotificationChannel(CHANNEL_BACKUP_RESTORE_COMPLETE, IMPORTANCE_HIGH) {
|
||||
setName(context.getString(R.string.channel_complete))
|
||||
setGroup(GROUP_BACKUP_RESTORE)
|
||||
setShowBadge(false)
|
||||
setSound(null, null)
|
||||
},
|
||||
NotificationChannel(
|
||||
CHANNEL_CRASH_LOGS,
|
||||
context.getString(R.string.channel_crash_logs),
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
),
|
||||
NotificationChannel(
|
||||
CHANNEL_INCOGNITO_MODE,
|
||||
context.getString(R.string.pref_incognito_mode),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
).forEach(context.notificationManager::createNotificationChannel)
|
||||
buildNotificationChannel(CHANNEL_CRASH_LOGS, IMPORTANCE_HIGH) {
|
||||
setName(context.getString(R.string.channel_crash_logs))
|
||||
},
|
||||
buildNotificationChannel(CHANNEL_INCOGNITO_MODE, IMPORTANCE_LOW) {
|
||||
setName(context.getString(R.string.pref_incognito_mode))
|
||||
},
|
||||
)
|
||||
notificationService.createNotificationChannelsCompat(channelList)
|
||||
|
||||
// Delete old notification channels
|
||||
deprecatedChannels.forEach(context.notificationManager::deleteNotificationChannel)
|
||||
deprecatedChannels.forEach(notificationService::deleteNotificationChannel)
|
||||
}
|
||||
}
|
||||
|
@@ -53,6 +53,8 @@ object PreferenceKeys {
|
||||
|
||||
const val grayscale = "pref_grayscale"
|
||||
|
||||
const val invertedColors = "pref_inverted_colors"
|
||||
|
||||
const val defaultReadingMode = "pref_default_reading_mode_key"
|
||||
|
||||
const val defaultOrientationType = "pref_default_orientation_type_key"
|
||||
@@ -87,6 +89,8 @@ object PreferenceKeys {
|
||||
|
||||
const val showNavigationOverlayOnStart = "reader_navigation_overlay_on_start"
|
||||
|
||||
const val readerHideThreshold = "reader_hide_threshold"
|
||||
|
||||
const val webtoonSidePadding = "webtoon_side_padding"
|
||||
|
||||
const val portraitColumns = "pref_library_columns_portrait_key"
|
||||
@@ -175,11 +179,11 @@ object PreferenceKeys {
|
||||
|
||||
const val downloadNewCategories = "download_new_categories"
|
||||
const val downloadNewCategoriesExclude = "download_new_categories_exclude"
|
||||
const val removeExcludeCategories = "remove_exclude_categories"
|
||||
|
||||
const val libraryDisplayMode = "pref_display_mode_library"
|
||||
|
||||
const val lang = "app_language"
|
||||
|
||||
const val relativeTime: String = "relative_time"
|
||||
const val dateFormat = "app_date_format"
|
||||
|
||||
const val defaultCategory = "default_category"
|
||||
@@ -220,6 +224,8 @@ object PreferenceKeys {
|
||||
|
||||
const val incognitoMode = "incognito_mode"
|
||||
|
||||
const val tabletUiMode = "tablet_ui_mode"
|
||||
|
||||
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
||||
|
||||
fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"
|
||||
|
@@ -22,15 +22,16 @@ object PreferenceValues {
|
||||
/* ktlint-enable experimental:enum-entry-name-case */
|
||||
|
||||
enum class AppTheme(val titleResId: Int?) {
|
||||
DEFAULT(R.string.theme_default),
|
||||
DEFAULT(R.string.label_default),
|
||||
MONET(R.string.theme_monet),
|
||||
BLUE(R.string.theme_blue),
|
||||
GREEN_APPLE(R.string.theme_greenapple),
|
||||
MIDNIGHT_DUSK(R.string.theme_midnightdusk),
|
||||
STRAWBERRY_DAIQUIRI(R.string.theme_strawberrydaiquiri),
|
||||
TAKO(R.string.theme_tako),
|
||||
YINYANG(R.string.theme_yinyang),
|
||||
YOTSUBA(R.string.theme_yotsuba),
|
||||
TAKO(R.string.theme_tako),
|
||||
GREEN_APPLE(R.string.theme_greenapple),
|
||||
TEALTURQUOISE(R.string.theme_tealturquoise),
|
||||
YINYANG(R.string.theme_yinyang),
|
||||
BLUE(R.string.theme_blue),
|
||||
|
||||
// Deprecated
|
||||
DARK_BLUE(null),
|
||||
@@ -43,4 +44,17 @@ object PreferenceValues {
|
||||
VERTICAL(shouldInvertVertical = true),
|
||||
BOTH(shouldInvertHorizontal = true, shouldInvertVertical = true),
|
||||
}
|
||||
|
||||
enum class ReaderHideThreshold(val threshold: Int) {
|
||||
HIGHEST(5),
|
||||
HIGH(13),
|
||||
LOW(31),
|
||||
LOWEST(47),
|
||||
}
|
||||
|
||||
enum class TabletUiMode {
|
||||
ALWAYS,
|
||||
LANDSCAPE,
|
||||
NEVER,
|
||||
}
|
||||
}
|
||||
|
@@ -9,7 +9,7 @@ import com.tfcporciuncula.flow.FlowSharedPreferences
|
||||
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.ThemeMode.*
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.ThemeMode.system
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
|
||||
@@ -17,6 +17,7 @@ import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
|
||||
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||
import eu.kanade.tachiyomi.util.system.isTablet
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
@@ -129,6 +130,8 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun grayscale() = flowPrefs.getBoolean(Keys.grayscale, false)
|
||||
|
||||
fun invertedColors() = flowPrefs.getBoolean(Keys.invertedColors, false)
|
||||
|
||||
fun defaultReadingMode() = prefs.getInt(Keys.defaultReadingMode, ReadingModeType.RIGHT_TO_LEFT.flagValue)
|
||||
|
||||
fun defaultOrientationType() = prefs.getInt(Keys.defaultOrientationType, OrientationType.FREE.flagValue)
|
||||
@@ -167,6 +170,8 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun showNavigationOverlayOnStart() = flowPrefs.getBoolean(Keys.showNavigationOverlayOnStart, false)
|
||||
|
||||
fun readerHideTreshold() = flowPrefs.getEnum(Keys.readerHideThreshold, Values.ReaderHideThreshold.LOW)
|
||||
|
||||
fun portraitColumns() = flowPrefs.getInt(Keys.portraitColumns, 0)
|
||||
|
||||
fun landscapeColumns() = flowPrefs.getInt(Keys.landscapeColumns, 0)
|
||||
@@ -204,6 +209,8 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun backupsDirectory() = flowPrefs.getString(Keys.backupDirectory, defaultBackupDir.toString())
|
||||
|
||||
fun relativeTime() = flowPrefs.getInt(Keys.relativeTime, 7)
|
||||
|
||||
fun dateFormat(format: String = flowPrefs.getString(Keys.dateFormat, "").get()): DateFormat = when (format) {
|
||||
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
|
||||
else -> SimpleDateFormat(format, Locale.getDefault())
|
||||
@@ -225,6 +232,8 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun removeBookmarkedChapters() = prefs.getBoolean(Keys.removeBookmarkedChapters, false)
|
||||
|
||||
fun removeExcludeCategories() = flowPrefs.getStringSet(Keys.removeExcludeCategories, emptySet())
|
||||
|
||||
fun libraryUpdateInterval() = flowPrefs.getInt(Keys.libraryUpdateInterval, 24)
|
||||
|
||||
fun libraryUpdateRestriction() = flowPrefs.getStringSet(Keys.libraryUpdateRestriction, setOf(UNMETERED_NETWORK))
|
||||
@@ -267,6 +276,7 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun extensionUpdatesCount() = flowPrefs.getInt("ext_updates_count", 0)
|
||||
|
||||
fun lastAppCheck() = flowPrefs.getLong("last_app_check", 0)
|
||||
fun lastExtCheck() = flowPrefs.getLong("last_ext_check", 0)
|
||||
|
||||
fun searchPinnedSourcesOnly() = prefs.getBoolean(Keys.searchPinnedSourcesOnly, false)
|
||||
@@ -280,8 +290,6 @@ class PreferencesHelper(val context: Context) {
|
||||
fun downloadNewCategories() = flowPrefs.getStringSet(Keys.downloadNewCategories, emptySet())
|
||||
fun downloadNewCategoriesExclude() = flowPrefs.getStringSet(Keys.downloadNewCategoriesExclude, emptySet())
|
||||
|
||||
fun lang() = flowPrefs.getString(Keys.lang, "")
|
||||
|
||||
fun defaultCategory() = prefs.getInt(Keys.defaultCategory, -1)
|
||||
|
||||
fun categorisedDisplaySettings() = flowPrefs.getBoolean(Keys.categorizedDisplay, false)
|
||||
@@ -312,6 +320,11 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun incognitoMode() = flowPrefs.getBoolean(Keys.incognitoMode, false)
|
||||
|
||||
fun tabletUiMode() = flowPrefs.getEnum(
|
||||
Keys.tabletUiMode,
|
||||
if (context.applicationContext.isTablet()) Values.TabletUiMode.ALWAYS else Values.TabletUiMode.NEVER
|
||||
)
|
||||
|
||||
fun setChapterSettingsDefault(manga: Manga) {
|
||||
prefs.edit {
|
||||
putInt(Keys.defaultChapterFilterByRead, manga.readFilter)
|
||||
|
@@ -182,6 +182,7 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
||||
override suspend fun refresh(track: Track): Track {
|
||||
val remoteTrack = api.getLibManga(track, getUsername().toInt())
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.title = remoteTrack.title
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
return track
|
||||
}
|
||||
|
@@ -48,7 +48,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
put("query", query)
|
||||
putJsonObject("variables") {
|
||||
put("mangaId", track.media_id)
|
||||
put("progress", track.last_chapter_read)
|
||||
put("progress", track.last_chapter_read.toInt())
|
||||
put("status", track.toAnilistStatus())
|
||||
}
|
||||
}
|
||||
@@ -89,7 +89,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
put("query", query)
|
||||
putJsonObject("variables") {
|
||||
put("listId", track.library_id)
|
||||
put("progress", track.last_chapter_read)
|
||||
put("progress", track.last_chapter_read.toInt())
|
||||
put("status", track.toAnilistStatus())
|
||||
put("score", track.score.toInt())
|
||||
put("startedAt", createDate(track.started_reading_date))
|
||||
@@ -110,12 +110,12 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
|
||||
|id
|
||||
|title {
|
||||
|romaji
|
||||
|userPreferred
|
||||
|}
|
||||
|coverImage {
|
||||
|large
|
||||
|}
|
||||
|type
|
||||
|format
|
||||
|status
|
||||
|chapters
|
||||
|description
|
||||
@@ -175,12 +175,12 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|media {
|
||||
|id
|
||||
|title {
|
||||
|romaji
|
||||
|userPreferred
|
||||
|}
|
||||
|coverImage {
|
||||
|large
|
||||
|}
|
||||
|type
|
||||
|format
|
||||
|status
|
||||
|chapters
|
||||
|description
|
||||
@@ -264,10 +264,10 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
private fun jsonToALManga(struct: JsonObject): ALManga {
|
||||
return ALManga(
|
||||
struct["id"]!!.jsonPrimitive.int,
|
||||
struct["title"]!!.jsonObject["romaji"]!!.jsonPrimitive.content,
|
||||
struct["title"]!!.jsonObject["userPreferred"]!!.jsonPrimitive.content,
|
||||
struct["coverImage"]!!.jsonObject["large"]!!.jsonPrimitive.content,
|
||||
struct["description"]!!.jsonPrimitive.contentOrNull,
|
||||
struct["type"]!!.jsonPrimitive.content,
|
||||
struct["format"]!!.jsonPrimitive.content.replace("_", "-"),
|
||||
struct["status"]!!.jsonPrimitive.contentOrNull ?: "",
|
||||
parseDate(struct, "startDate"),
|
||||
struct["chapters"]!!.jsonPrimitive.intOrNull ?: 0
|
||||
|
@@ -10,10 +10,10 @@ import java.util.Locale
|
||||
|
||||
data class ALManga(
|
||||
val media_id: Int,
|
||||
val title_romaji: String,
|
||||
val title_user_pref: String,
|
||||
val image_url_lge: String,
|
||||
val description: String?,
|
||||
val type: String,
|
||||
val format: String,
|
||||
val publishing_status: String,
|
||||
val start_date_fuzzy: Long,
|
||||
val total_chapters: Int
|
||||
@@ -21,13 +21,13 @@ data class ALManga(
|
||||
|
||||
fun toTrack() = TrackSearch.create(TrackManager.ANILIST).apply {
|
||||
media_id = this@ALManga.media_id
|
||||
title = title_romaji
|
||||
title = title_user_pref
|
||||
total_chapters = this@ALManga.total_chapters
|
||||
cover_url = image_url_lge
|
||||
summary = description ?: ""
|
||||
tracking_url = AnilistApi.mangaUrl(media_id)
|
||||
publishing_status = this@ALManga.publishing_status
|
||||
publishing_type = type
|
||||
publishing_type = format
|
||||
if (start_date_fuzzy != 0L) {
|
||||
start_date = try {
|
||||
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||
@@ -51,11 +51,12 @@ data class ALUserManga(
|
||||
|
||||
fun toTrack() = Track.create(TrackManager.ANILIST).apply {
|
||||
media_id = manga.media_id
|
||||
title = manga.title_user_pref
|
||||
status = toTrackStatus()
|
||||
score = score_raw.toFloat()
|
||||
started_reading_date = start_date_fuzzy
|
||||
finished_reading_date = completed_date_fuzzy
|
||||
last_chapter_read = chapters_read
|
||||
last_chapter_read = chapters_read.toFloat()
|
||||
library_id = this@ALUserManga.library_id
|
||||
total_chapters = manga.total_chapters
|
||||
}
|
||||
|
@@ -55,7 +55,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
||||
|
||||
// chapter update
|
||||
val body = FormBody.Builder()
|
||||
.add("watched_eps", track.last_chapter_read.toString())
|
||||
.add("watched_eps", track.last_chapter_read.toInt().toString())
|
||||
.build()
|
||||
authClient.newCall(
|
||||
POST(
|
||||
@@ -143,7 +143,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
||||
} else {
|
||||
json.decodeFromString<Collection>(responseBody).let {
|
||||
track.status = it.status?.id!!
|
||||
track.last_chapter_read = it.ep_status!!
|
||||
track.last_chapter_read = it.ep_status!!.toFloat()
|
||||
track.score = it.rating!!
|
||||
track
|
||||
}
|
||||
|
@@ -0,0 +1,50 @@
|
||||
package eu.kanade.tachiyomi.data.track.job
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.edit
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import timber.log.Timber
|
||||
|
||||
class DelayedTrackingStore(context: Context) {
|
||||
|
||||
/**
|
||||
* Preference file where queued tracking updates are stored.
|
||||
*/
|
||||
private val preferences = context.getSharedPreferences("tracking_queue", Context.MODE_PRIVATE)
|
||||
|
||||
fun addItem(track: Track) {
|
||||
val trackId = track.id.toString()
|
||||
val (_, lastChapterRead) = preferences.getString(trackId, "0:0.0")!!.split(":")
|
||||
if (track.last_chapter_read > lastChapterRead.toFloat()) {
|
||||
val value = "${track.manga_id}:${track.last_chapter_read}"
|
||||
Timber.i("Queuing track item: $trackId, $value")
|
||||
preferences.edit {
|
||||
putString(trackId, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
preferences.edit {
|
||||
clear()
|
||||
}
|
||||
}
|
||||
|
||||
fun getItems(): List<DelayedTrackingItem> {
|
||||
return (preferences.all as Map<String, String>).entries
|
||||
.map {
|
||||
val (mangaId, lastChapterRead) = it.value.split(":")
|
||||
DelayedTrackingItem(
|
||||
trackId = it.key.toLong(),
|
||||
mangaId = mangaId.toLong(),
|
||||
lastChapterRead = lastChapterRead.toFloat(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class DelayedTrackingItem(
|
||||
val trackId: Long,
|
||||
val mangaId: Long,
|
||||
val lastChapterRead: Float,
|
||||
)
|
||||
}
|
@@ -0,0 +1,75 @@
|
||||
package eu.kanade.tachiyomi.data.track.job
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class DelayedTrackingUpdateJob(context: Context, workerParams: WorkerParameters) :
|
||||
CoroutineWorker(context, workerParams) {
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val db = Injekt.get<DatabaseHelper>()
|
||||
val trackManager = Injekt.get<TrackManager>()
|
||||
val delayedTrackingStore = Injekt.get<DelayedTrackingStore>()
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val tracks = delayedTrackingStore.getItems().mapNotNull {
|
||||
val manga = db.getManga(it.mangaId).executeAsBlocking() ?: return@withContext
|
||||
db.getTracks(manga).executeAsBlocking()
|
||||
.find { track -> track.id == it.trackId }
|
||||
?.also { track ->
|
||||
track.last_chapter_read = it.lastChapterRead
|
||||
}
|
||||
}
|
||||
|
||||
tracks.forEach { track ->
|
||||
try {
|
||||
val service = trackManager.getService(track.sync_id)
|
||||
if (service != null && service.isLogged) {
|
||||
service.update(track, true)
|
||||
db.insertTrack(track).executeAsBlocking()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
|
||||
delayedTrackingStore.clear()
|
||||
}
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "DelayedTrackingUpdate"
|
||||
|
||||
fun setupTask(context: Context) {
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
|
||||
val request = OneTimeWorkRequestBuilder<DelayedTrackingUpdateJob>()
|
||||
.setConstraints(constraints)
|
||||
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 20, TimeUnit.SECONDS)
|
||||
.addTag(TAG)
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance(context)
|
||||
.enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, request)
|
||||
}
|
||||
}
|
||||
}
|
@@ -36,7 +36,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
||||
put("type", "libraryEntries")
|
||||
putJsonObject("attributes") {
|
||||
put("status", track.toKitsuStatus())
|
||||
put("progress", track.last_chapter_read)
|
||||
put("progress", track.last_chapter_read.toInt())
|
||||
}
|
||||
putJsonObject("relationships") {
|
||||
putJsonObject("user") {
|
||||
@@ -82,7 +82,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
||||
put("id", track.media_id)
|
||||
putJsonObject("attributes") {
|
||||
put("status", track.toKitsuStatus())
|
||||
put("progress", track.last_chapter_read)
|
||||
put("progress", track.last_chapter_read.toInt())
|
||||
put("ratingTwenty", track.toKitsuScore())
|
||||
put("startedAt", KitsuDateHelper.convert(track.started_reading_date))
|
||||
put("finishedAt", KitsuDateHelper.convert(track.finished_reading_date))
|
||||
|
@@ -25,7 +25,7 @@ class KitsuSearchManga(obj: JsonObject) {
|
||||
// posterImage is sometimes a jsonNull object instead
|
||||
null
|
||||
}
|
||||
private val synopsis = obj["synopsis"]!!.jsonPrimitive.content
|
||||
private val synopsis = obj["synopsis"]?.jsonPrimitive?.contentOrNull
|
||||
private var startDate = obj["startDate"]?.jsonPrimitive?.contentOrNull?.let {
|
||||
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||
outputDf.format(Date(it.toLong() * 1000))
|
||||
@@ -38,7 +38,7 @@ class KitsuSearchManga(obj: JsonObject) {
|
||||
title = canonicalTitle
|
||||
total_chapters = chapterCount ?: 0
|
||||
cover_url = original ?: ""
|
||||
summary = synopsis
|
||||
summary = synopsis ?: ""
|
||||
tracking_url = KitsuApi.mangaUrl(media_id)
|
||||
publishing_status = if (endDate == null) {
|
||||
"Publishing"
|
||||
@@ -79,7 +79,7 @@ class KitsuLibManga(obj: JsonObject, manga: JsonObject) {
|
||||
finished_reading_date = KitsuDateHelper.parse(finishedAt)
|
||||
status = toTrackStatus()
|
||||
score = ratingTwenty?.let { it.toInt() / 2f } ?: 0f
|
||||
last_chapter_read = progress
|
||||
last_chapter_read = progress.toFloat()
|
||||
}
|
||||
|
||||
private fun toTrackStatus() = when (status) {
|
||||
|
@@ -38,9 +38,11 @@ class KomgaApi(private val client: OkHttpClient) {
|
||||
}
|
||||
|
||||
val progress = client
|
||||
.newCall(GET("$url/read-progress/tachiyomi"))
|
||||
.await()
|
||||
.parseAs<ReadProgressDto>()
|
||||
.newCall(GET("${url.replace("/api/v1/series/", "/api/v2/series/")}/read-progress/tachiyomi"))
|
||||
.await().let {
|
||||
if (url.contains("/api/v1/series/")) it.parseAs<ReadProgressV2Dto>()
|
||||
else it.parseAs<ReadProgressDto>().toV2()
|
||||
}
|
||||
|
||||
track.apply {
|
||||
cover_url = "$url/thumbnail"
|
||||
@@ -51,7 +53,7 @@ class KomgaApi(private val client: OkHttpClient) {
|
||||
progress.booksReadCount -> Komga.COMPLETED
|
||||
else -> Komga.READING
|
||||
}
|
||||
last_chapter_read = progress.lastReadContinuousIndex
|
||||
last_chapter_read = progress.lastReadContinuousNumberSort
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e, "Could not get item: $url")
|
||||
@@ -60,11 +62,14 @@ class KomgaApi(private val client: OkHttpClient) {
|
||||
}
|
||||
|
||||
suspend fun updateProgress(track: Track): Track {
|
||||
val progress = ReadProgressUpdateDto(track.last_chapter_read)
|
||||
val payload = json.encodeToString(progress)
|
||||
val payload = if (track.tracking_url.contains("/api/v1/series/")) {
|
||||
json.encodeToString(ReadProgressUpdateV2Dto(track.last_chapter_read))
|
||||
} else {
|
||||
json.encodeToString(ReadProgressUpdateDto(track.last_chapter_read.toInt()))
|
||||
}
|
||||
client.newCall(
|
||||
Request.Builder()
|
||||
.url("${track.tracking_url}/read-progress/tachiyomi")
|
||||
.url("${track.tracking_url.replace("/api/v1/series/", "/api/v2/series/")}/read-progress/tachiyomi")
|
||||
.put(payload.toRequestBody("application/json".toMediaType()))
|
||||
.build()
|
||||
)
|
||||
|
@@ -63,6 +63,11 @@ data class ReadProgressUpdateDto(
|
||||
val lastBookRead: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReadProgressUpdateV2Dto(
|
||||
val lastBookNumberSortRead: Float,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReadListDto(
|
||||
val id: String,
|
||||
@@ -80,4 +85,21 @@ data class ReadProgressDto(
|
||||
val booksUnreadCount: Int,
|
||||
val booksInProgressCount: Int,
|
||||
val lastReadContinuousIndex: Int,
|
||||
) {
|
||||
fun toV2() = ReadProgressV2Dto(
|
||||
booksCount,
|
||||
booksReadCount,
|
||||
booksUnreadCount,
|
||||
booksInProgressCount,
|
||||
lastReadContinuousIndex.toFloat()
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ReadProgressV2Dto(
|
||||
val booksCount: Int,
|
||||
val booksReadCount: Int,
|
||||
val booksUnreadCount: Int,
|
||||
val booksInProgressCount: Int,
|
||||
val lastReadContinuousNumberSort: Float,
|
||||
)
|
||||
|
@@ -16,7 +16,7 @@ class TrackSearch : Track {
|
||||
|
||||
override lateinit var title: String
|
||||
|
||||
override var last_chapter_read: Int = 0
|
||||
override var last_chapter_read: Float = 0F
|
||||
|
||||
override var total_chapters: Int = 0
|
||||
|
||||
|
@@ -16,6 +16,7 @@ import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.boolean
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.float
|
||||
import kotlinx.serialization.json.int
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
@@ -117,7 +118,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
||||
.add("status", track.toMyAnimeListStatus() ?: "reading")
|
||||
.add("is_rereading", (track.status == MyAnimeList.REREADING).toString())
|
||||
.add("score", track.score.toString())
|
||||
.add("num_chapters_read", track.last_chapter_read.toString())
|
||||
.add("num_chapters_read", track.last_chapter_read.toInt().toString())
|
||||
convertToIsoDate(track.started_reading_date)?.let {
|
||||
formBodyBuilder.add("start_date", it)
|
||||
}
|
||||
@@ -205,7 +206,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
||||
return track.apply {
|
||||
val isRereading = obj["is_rereading"]!!.jsonPrimitive.boolean
|
||||
status = if (isRereading) MyAnimeList.REREADING else getStatus(obj["status"]!!.jsonPrimitive.content)
|
||||
last_chapter_read = obj["num_chapters_read"]!!.jsonPrimitive.int
|
||||
last_chapter_read = obj["num_chapters_read"]!!.jsonPrimitive.float
|
||||
score = obj["score"]!!.jsonPrimitive.int.toFloat()
|
||||
obj["start_date"]?.let {
|
||||
started_reading_date = parseDate(it.jsonPrimitive.content)
|
||||
|
@@ -15,6 +15,7 @@ import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.float
|
||||
import kotlinx.serialization.json.int
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
@@ -35,7 +36,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
||||
put("user_id", user_id)
|
||||
put("target_id", track.media_id)
|
||||
put("target_type", "Manga")
|
||||
put("chapters", track.last_chapter_read)
|
||||
put("chapters", track.last_chapter_read.toInt())
|
||||
put("score", track.score.toInt())
|
||||
put("status", track.toShikimoriStatus())
|
||||
}
|
||||
@@ -89,7 +90,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
||||
title = mangas["name"]!!.jsonPrimitive.content
|
||||
media_id = obj["id"]!!.jsonPrimitive.int
|
||||
total_chapters = mangas["chapters"]!!.jsonPrimitive.int
|
||||
last_chapter_read = obj["chapters"]!!.jsonPrimitive.int
|
||||
last_chapter_read = obj["chapters"]!!.jsonPrimitive.float
|
||||
score = (obj["score"]!!.jsonPrimitive.int).toFloat()
|
||||
status = toTrackStatus(obj["status"]!!.jsonPrimitive.content)
|
||||
tracking_url = baseUrl + mangas["url"]!!.jsonPrimitive.content
|
||||
|
@@ -1,16 +1,19 @@
|
||||
package eu.kanade.tachiyomi.data.updater
|
||||
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.Date
|
||||
|
||||
class GithubUpdateChecker {
|
||||
class AppUpdateChecker {
|
||||
|
||||
private val networkService: NetworkHelper by injectLazy()
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
private val repo: String by lazy {
|
||||
if (BuildConfig.DEBUG) {
|
||||
@@ -20,18 +23,20 @@ class GithubUpdateChecker {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun checkForUpdate(): GithubUpdateResult {
|
||||
suspend fun checkForUpdate(): AppUpdateResult {
|
||||
return withIOContext {
|
||||
networkService.client
|
||||
.newCall(GET("https://api.github.com/repos/$repo/releases/latest"))
|
||||
.await()
|
||||
.parseAs<GithubRelease>()
|
||||
.let {
|
||||
preferences.lastAppCheck().set(Date().time)
|
||||
|
||||
// Check if latest version is different from current version
|
||||
if (isNewVersion(it.version)) {
|
||||
GithubUpdateResult.NewUpdate(it)
|
||||
AppUpdateResult.NewUpdate(it)
|
||||
} else {
|
||||
GithubUpdateResult.NoNewUpdate
|
||||
AppUpdateResult.NoNewUpdate
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.updater
|
||||
|
||||
sealed class AppUpdateResult {
|
||||
class NewUpdate(val release: GithubRelease) : AppUpdateResult()
|
||||
object NoNewUpdate : AppUpdateResult()
|
||||
}
|
@@ -1,6 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.updater
|
||||
|
||||
sealed class GithubUpdateResult {
|
||||
class NewUpdate(val release: GithubRelease) : GithubUpdateResult()
|
||||
object NoNewUpdate : GithubUpdateResult()
|
||||
}
|
@@ -8,6 +8,7 @@ import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@@ -16,9 +17,9 @@ class UpdaterJob(private val context: Context, workerParams: WorkerParameters) :
|
||||
|
||||
override fun doWork() = runBlocking {
|
||||
try {
|
||||
val result = GithubUpdateChecker().checkForUpdate()
|
||||
val result = AppUpdateChecker().checkForUpdate()
|
||||
|
||||
if (result is GithubUpdateResult.NewUpdate) {
|
||||
if (result is AppUpdateResult.NewUpdate) {
|
||||
UpdaterNotifier(context).promptUpdate(result.release.getDownloadLink())
|
||||
}
|
||||
Result.success()
|
||||
@@ -31,12 +32,18 @@ class UpdaterJob(private val context: Context, workerParams: WorkerParameters) :
|
||||
private const val TAG = "UpdateChecker"
|
||||
|
||||
fun setupTask(context: Context) {
|
||||
// Never check for updates in debug builds that don't include the updater
|
||||
if (BuildConfig.DEBUG && !BuildConfig.INCLUDE_UPDATER) {
|
||||
cancelTask(context)
|
||||
return
|
||||
}
|
||||
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
|
||||
val request = PeriodicWorkRequestBuilder<UpdaterJob>(
|
||||
3,
|
||||
7,
|
||||
TimeUnit.DAYS,
|
||||
3,
|
||||
TimeUnit.HOURS
|
||||
|
@@ -73,9 +73,9 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam
|
||||
.build()
|
||||
|
||||
val request = PeriodicWorkRequestBuilder<ExtensionUpdateJob>(
|
||||
12,
|
||||
TimeUnit.HOURS,
|
||||
1,
|
||||
2,
|
||||
TimeUnit.DAYS,
|
||||
3,
|
||||
TimeUnit.HOURS
|
||||
)
|
||||
.addTag(TAG)
|
||||
|
@@ -14,6 +14,7 @@ import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.int
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.long
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.Date
|
||||
|
||||
@@ -67,7 +68,7 @@ internal class ExtensionGithubApi {
|
||||
val pkgName = element.jsonObject["pkg"]!!.jsonPrimitive.content
|
||||
val apkName = element.jsonObject["apk"]!!.jsonPrimitive.content
|
||||
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
|
||||
val versionCode = element.jsonObject["code"]!!.jsonPrimitive.int
|
||||
val versionCode = element.jsonObject["code"]!!.jsonPrimitive.long
|
||||
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")}"
|
||||
|
@@ -7,7 +7,7 @@ sealed class Extension {
|
||||
abstract val name: String
|
||||
abstract val pkgName: String
|
||||
abstract val versionName: String
|
||||
abstract val versionCode: Int
|
||||
abstract val versionCode: Long
|
||||
abstract val lang: String?
|
||||
abstract val isNsfw: Boolean
|
||||
|
||||
@@ -15,7 +15,7 @@ sealed class Extension {
|
||||
override val name: String,
|
||||
override val pkgName: String,
|
||||
override val versionName: String,
|
||||
override val versionCode: Int,
|
||||
override val versionCode: Long,
|
||||
override val lang: String,
|
||||
override val isNsfw: Boolean,
|
||||
val pkgFactory: String?,
|
||||
@@ -29,7 +29,7 @@ sealed class Extension {
|
||||
override val name: String,
|
||||
override val pkgName: String,
|
||||
override val versionName: String,
|
||||
override val versionCode: Int,
|
||||
override val versionCode: Long,
|
||||
override val lang: String,
|
||||
override val isNsfw: Boolean,
|
||||
val apkName: String,
|
||||
@@ -40,7 +40,7 @@ sealed class Extension {
|
||||
override val name: String,
|
||||
override val pkgName: String,
|
||||
override val versionName: String,
|
||||
override val versionCode: Int,
|
||||
override val versionCode: Long,
|
||||
val signatureHash: String,
|
||||
override val lang: String? = null,
|
||||
override val isNsfw: Boolean = false
|
||||
|
@@ -45,8 +45,6 @@ class ExtensionInstallActivity : Activity() {
|
||||
val extensionManager = Injekt.get<ExtensionManager>()
|
||||
extensionManager.setInstallationResult(downloadId, success)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val INSTALL_REQUEST_CODE = 500
|
||||
}
|
||||
}
|
||||
|
||||
private const val INSTALL_REQUEST_CODE = 500
|
||||
|
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.core.content.pm.PackageInfoCompat
|
||||
import dalvik.system.PathClassLoader
|
||||
import eu.kanade.tachiyomi.annotations.Nsfw
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
@@ -103,7 +104,7 @@ internal object ExtensionLoader {
|
||||
|
||||
val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Tachiyomi: ")
|
||||
val versionName = pkgInfo.versionName
|
||||
val versionCode = pkgInfo.versionCode
|
||||
val versionCode = PackageInfoCompat.getLongVersionCode(pkgInfo)
|
||||
|
||||
if (versionName.isNullOrEmpty()) {
|
||||
val exception = Exception("Missing versionName for extension $extName")
|
||||
|
@@ -11,6 +11,7 @@ import java.net.InetAddress
|
||||
|
||||
const val PREF_DOH_CLOUDFLARE = 1
|
||||
const val PREF_DOH_GOOGLE = 2
|
||||
const val PREF_DOH_ADGUARD = 3
|
||||
|
||||
fun OkHttpClient.Builder.dohCloudflare() = dns(
|
||||
DnsOverHttps.Builder().client(build())
|
||||
@@ -38,3 +39,16 @@ fun OkHttpClient.Builder.dohGoogle() = dns(
|
||||
)
|
||||
.build()
|
||||
)
|
||||
|
||||
// AdGuard "Default" DNS works too but for the sake of making sure no site is blacklisted, i picked "Unfiltered"
|
||||
fun OkHttpClient.Builder.dohAdGuard() = dns(
|
||||
DnsOverHttps.Builder().client(build())
|
||||
.url("https://dns-unfiltered.adguard.com/dns-query".toHttpUrl())
|
||||
.bootstrapDnsHosts(
|
||||
InetAddress.getByName("94.140.14.140"),
|
||||
InetAddress.getByName("94.140.14.141"),
|
||||
InetAddress.getByName("2a10:50c0::1:ff"),
|
||||
InetAddress.getByName("2a10:50c0::2:ff"),
|
||||
)
|
||||
.build()
|
||||
)
|
||||
|
@@ -41,6 +41,7 @@ class NetworkHelper(context: Context) {
|
||||
when (preferences.dohProvider()) {
|
||||
PREF_DOH_CLOUDFLARE -> builder.dohCloudflare()
|
||||
PREF_DOH_GOOGLE -> builder.dohGoogle()
|
||||
PREF_DOH_ADGUARD -> builder.dohAdGuard()
|
||||
}
|
||||
|
||||
return builder
|
||||
|
@@ -15,10 +15,8 @@ import rx.Producer
|
||||
import rx.Subscription
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.fullType
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
val jsonMime = "application/json; charset=utf-8".toMediaType()
|
||||
|
@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.source
|
||||
|
||||
import android.content.Context
|
||||
import com.github.junrar.Archive
|
||||
import com.google.gson.JsonParser
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
@@ -15,8 +14,16 @@ import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.storage.EpubFile
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import kotlinx.serialization.json.intOrNull
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import rx.Observable
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.InputStream
|
||||
@@ -68,6 +75,8 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
||||
}
|
||||
}
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
override val id = ID
|
||||
override val name = context.getString(R.string.local_source)
|
||||
override val lang = ""
|
||||
@@ -157,16 +166,15 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
||||
.flatten()
|
||||
.firstOrNull { it.extension == "json" }
|
||||
?.apply {
|
||||
val reader = this.inputStream().bufferedReader()
|
||||
val json = JsonParser.parseReader(reader).asJsonObject
|
||||
val obj = json.decodeFromStream<JsonObject>(inputStream())
|
||||
|
||||
manga.title = json["title"]?.asString ?: manga.title
|
||||
manga.author = json["author"]?.asString ?: manga.author
|
||||
manga.artist = json["artist"]?.asString ?: manga.artist
|
||||
manga.description = json["description"]?.asString ?: manga.description
|
||||
manga.genre = json["genre"]?.asJsonArray?.joinToString(", ") { it.asString }
|
||||
manga.title = obj["title"]?.jsonPrimitive?.contentOrNull ?: manga.title
|
||||
manga.author = obj["author"]?.jsonPrimitive?.contentOrNull ?: manga.author
|
||||
manga.artist = obj["artist"]?.jsonPrimitive?.contentOrNull ?: manga.artist
|
||||
manga.description = obj["description"]?.jsonPrimitive?.contentOrNull ?: manga.description
|
||||
manga.genre = obj["genre"]?.jsonArray?.joinToString(", ") { it.jsonPrimitive.content }
|
||||
?: manga.genre
|
||||
manga.status = json["status"]?.asInt ?: manga.status
|
||||
manga.status = obj["status"]?.jsonPrimitive?.intOrNull ?: manga.status
|
||||
}
|
||||
|
||||
return Observable.just(manga)
|
||||
|
@@ -5,7 +5,7 @@ import android.os.Bundle
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.util.system.prepareTabletUiContext
|
||||
import nucleus.view.NucleusAppCompatActivity
|
||||
|
||||
abstract class BaseRxActivity<VB : ViewBinding, P : BasePresenter<*>> : NucleusAppCompatActivity<P>() {
|
||||
@@ -16,7 +16,7 @@ abstract class BaseRxActivity<VB : ViewBinding, P : BasePresenter<*>> : NucleusA
|
||||
lateinit var binding: VB
|
||||
|
||||
override fun attachBaseContext(newBase: Context) {
|
||||
super.attachBaseContext(LocaleHelper.createLocaleWrapper(newBase))
|
||||
super.attachBaseContext(newBase.prepareTabletUiContext())
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
@@ -6,7 +6,7 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.util.system.prepareTabletUiContext
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
abstract class BaseThemedActivity : AppCompatActivity() {
|
||||
@@ -14,7 +14,7 @@ abstract class BaseThemedActivity : AppCompatActivity() {
|
||||
val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
override fun attachBaseContext(newBase: Context) {
|
||||
super.attachBaseContext(LocaleHelper.createLocaleWrapper(newBase))
|
||||
super.attachBaseContext(newBase.prepareTabletUiContext())
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -24,8 +24,13 @@ abstract class BaseThemedActivity : AppCompatActivity() {
|
||||
|
||||
companion object {
|
||||
fun AppCompatActivity.applyAppTheme(preferences: PreferencesHelper) {
|
||||
getThemeResIds(preferences.appTheme().get(), preferences.themeDarkAmoled().get())
|
||||
.forEach { setTheme(it) }
|
||||
}
|
||||
|
||||
fun getThemeResIds(appTheme: PreferenceValues.AppTheme, isAmoled: Boolean): List<Int> {
|
||||
val resIds = mutableListOf<Int>()
|
||||
when (preferences.appTheme().get()) {
|
||||
when (appTheme) {
|
||||
PreferenceValues.AppTheme.MONET -> {
|
||||
resIds += R.style.Theme_Tachiyomi_Monet
|
||||
}
|
||||
@@ -45,6 +50,9 @@ abstract class BaseThemedActivity : AppCompatActivity() {
|
||||
PreferenceValues.AppTheme.TAKO -> {
|
||||
resIds += R.style.Theme_Tachiyomi_Tako
|
||||
}
|
||||
PreferenceValues.AppTheme.TEALTURQUOISE -> {
|
||||
resIds += R.style.Theme_Tachiyomi_TealTurquoise
|
||||
}
|
||||
PreferenceValues.AppTheme.YINYANG -> {
|
||||
resIds += R.style.Theme_Tachiyomi_YinYang
|
||||
}
|
||||
@@ -56,13 +64,11 @@ abstract class BaseThemedActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
if (preferences.themeDarkAmoled().get()) {
|
||||
if (isAmoled) {
|
||||
resIds += R.style.ThemeOverlay_Tachiyomi_Amoled
|
||||
}
|
||||
|
||||
resIds.forEach {
|
||||
setTheme(it)
|
||||
}
|
||||
return resIds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,13 +1,13 @@
|
||||
package eu.kanade.tachiyomi.ui.base.controller
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import com.bluelinelabs.conductor.Router
|
||||
import com.bluelinelabs.conductor.RouterTransaction
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||
|
||||
fun Router.popControllerWithTag(tag: String): Boolean {
|
||||
val controller = getControllerWithTag(tag)
|
||||
@@ -34,10 +34,12 @@ fun Controller.withFadeTransaction(): RouterTransaction {
|
||||
}
|
||||
|
||||
fun Controller.openInBrowser(url: String) {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||
startActivity(intent)
|
||||
} catch (e: Throwable) {
|
||||
activity?.toast(e.message)
|
||||
}
|
||||
activity?.openInBrowser(url.toUri())
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [MainActivity]'s app bar height
|
||||
*/
|
||||
fun Controller.getMainAppBarHeight(): Int {
|
||||
return (activity as? MainActivity)?.binding?.appbar?.measuredHeight ?: 0
|
||||
}
|
||||
|
@@ -1,3 +1,3 @@
|
||||
package eu.kanade.tachiyomi.ui.base.controller
|
||||
|
||||
interface NoToolbarElevationController
|
||||
interface NoAppBarElevationController
|
@@ -1,3 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.base.controller
|
||||
|
||||
interface ToolbarLiftOnScrollController
|
@@ -92,6 +92,11 @@ class BrowseController :
|
||||
}
|
||||
|
||||
fun setExtensionUpdateBadge() {
|
||||
/* It's possible to switch to the Library controller by the time setExtensionUpdateBadge
|
||||
is called, resulting in a badge being put on the category tabs (if enabled).
|
||||
This check prevents that from happening */
|
||||
if (router.backstack.last().controller !is BrowseController) return
|
||||
|
||||
(activity as? MainActivity)?.binding?.tabs?.apply {
|
||||
val updates = preferences.extensionUpdatesCount().get()
|
||||
if (updates > 0) {
|
||||
|
@@ -72,7 +72,7 @@ open class ExtensionPresenter(
|
||||
(avail.lang in activeLangs || avail.lang == "all") &&
|
||||
(showNsfwExtensions || !avail.isNsfw)
|
||||
}
|
||||
.sortedBy { it.pkgName }
|
||||
.sortedBy { it.name }
|
||||
|
||||
if (updatesSorted.isNotEmpty()) {
|
||||
val header = ExtensionGroupItem(context.getString(R.string.ext_updates_pending), updatesSorted.size, true)
|
||||
|
@@ -33,13 +33,11 @@ class ExtensionTrustDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
|
||||
.create()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val SIGNATURE_KEY = "signature_key"
|
||||
const val PKGNAME_KEY = "pkgname_key"
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun trustSignature(signatureHash: String)
|
||||
fun uninstallExtension(pkgName: String)
|
||||
}
|
||||
}
|
||||
|
||||
private const val SIGNATURE_KEY = "signature_key"
|
||||
private const val PKGNAME_KEY = "pkgname_key"
|
||||
|
@@ -34,7 +34,6 @@ import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.getPreferenceKey
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.ToolbarLiftOnScrollController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.openInBrowser
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.util.preference.DSL
|
||||
@@ -49,8 +48,7 @@ import uy.kohesive.injekt.injectLazy
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
class ExtensionDetailsController(bundle: Bundle? = null) :
|
||||
NucleusController<ExtensionDetailControllerBinding, ExtensionDetailsPresenter>(bundle),
|
||||
ToolbarLiftOnScrollController {
|
||||
NucleusController<ExtensionDetailControllerBinding, ExtensionDetailsPresenter>(bundle) {
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
@@ -237,10 +235,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
|
||||
activity!!.theme.resolveAttribute(R.attr.preferenceTheme, tv, true)
|
||||
return ContextThemeWrapper(activity, tv.resourceId)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val PKGNAME_KEY = "pkg_name"
|
||||
|
||||
private const val URL_EXTENSION_COMMITS = "https://github.com/tachiyomiorg/tachiyomi-extensions/commits/master"
|
||||
}
|
||||
}
|
||||
|
||||
private const val PKGNAME_KEY = "pkg_name"
|
||||
private const val URL_EXTENSION_COMMITS = "https://github.com/tachiyomiorg/tachiyomi-extensions/commits/master"
|
||||
|
22
app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsHeaderAdapter.kt
22
app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsHeaderAdapter.kt
@@ -34,25 +34,25 @@ class ExtensionDetailsHeaderAdapter(private val presenter: ExtensionDetailsPrese
|
||||
val extension = presenter.extension ?: return
|
||||
val context = view.context
|
||||
|
||||
extension.getApplicationIcon(context)?.let { binding.extensionIcon.setImageDrawable(it) }
|
||||
binding.extensionTitle.text = extension.name
|
||||
binding.extensionVersion.text = context.getString(R.string.ext_version_info, extension.versionName)
|
||||
binding.extensionLang.text = context.getString(R.string.ext_language_info, LocaleHelper.getSourceDisplayName(extension.lang, context))
|
||||
binding.extensionNsfw.isVisible = extension.isNsfw
|
||||
binding.extensionPkg.text = extension.pkgName
|
||||
extension.getApplicationIcon(context)?.let { binding.icon.setImageDrawable(it) }
|
||||
binding.title.text = extension.name
|
||||
binding.version.text = context.getString(R.string.ext_version_info, extension.versionName)
|
||||
binding.lang.text = context.getString(R.string.ext_language_info, LocaleHelper.getSourceDisplayName(extension.lang, context))
|
||||
binding.nsfw.isVisible = extension.isNsfw
|
||||
binding.pkgname.text = extension.pkgName
|
||||
|
||||
binding.extensionUninstallButton.clicks()
|
||||
binding.btnUninstall.clicks()
|
||||
.onEach { presenter.uninstallExtension() }
|
||||
.launchIn(presenter.presenterScope)
|
||||
|
||||
if (extension.isObsolete) {
|
||||
binding.extensionWarningBanner.isVisible = true
|
||||
binding.extensionWarningBanner.setText(R.string.obsolete_extension_message)
|
||||
binding.warningBanner.isVisible = true
|
||||
binding.warningBanner.setText(R.string.obsolete_extension_message)
|
||||
}
|
||||
|
||||
if (extension.isUnofficial) {
|
||||
binding.extensionWarningBanner.isVisible = true
|
||||
binding.extensionWarningBanner.setText(R.string.unofficial_extension_message)
|
||||
binding.warningBanner.isVisible = true
|
||||
binding.warningBanner.setText(R.string.unofficial_extension_message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -27,6 +27,7 @@ import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.getPreferenceKey
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.widget.TachiyomiTextInputEditText.Companion.setIncognito
|
||||
import timber.log.Timber
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
@@ -113,6 +114,13 @@ class SourcePreferencesController(bundle: Bundle? = null) :
|
||||
pref.isIconSpaceReserved = false
|
||||
pref.order = Int.MAX_VALUE // reset to default order
|
||||
|
||||
// Apply incognito IME for EditTextPreference
|
||||
if (pref is EditTextPreference) {
|
||||
pref.setOnBindEditTextListener {
|
||||
it.setIncognito(viewScope)
|
||||
}
|
||||
}
|
||||
|
||||
newScreen.removePreference(pref)
|
||||
screen.addPreference(pref)
|
||||
}
|
||||
@@ -159,9 +167,7 @@ class SourcePreferencesController(bundle: Bundle? = null) :
|
||||
// [key] isn't useful since there may be duplicates
|
||||
return preferenceScreen!!.getPreference(lastOpenPreferencePosition!!) as T
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val SOURCE_ID = "source_id"
|
||||
const val LASTOPENPREFERENCE_KEY = "last_open_preference"
|
||||
}
|
||||
}
|
||||
|
||||
private const val SOURCE_ID = "source_id"
|
||||
private const val LASTOPENPREFERENCE_KEY = "last_open_preference"
|
||||
|
@@ -3,9 +3,7 @@ package eu.kanade.tachiyomi.ui.browse.migration.manga
|
||||
import android.view.View
|
||||
import coil.clear
|
||||
import coil.loadAny
|
||||
import coil.transform.RoundedCornersTransformation
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.databinding.SourceListItemBinding
|
||||
|
||||
class MigrationMangaHolder(
|
||||
@@ -24,11 +22,8 @@ class MigrationMangaHolder(
|
||||
fun bind(item: MigrationMangaItem) {
|
||||
binding.title.text = item.manga.title
|
||||
|
||||
// Update the cover.
|
||||
val radius = itemView.context.resources.getDimension(R.dimen.card_radius)
|
||||
// Update the cover
|
||||
binding.thumbnail.clear()
|
||||
binding.thumbnail.loadAny(item.manga) {
|
||||
transformations(RoundedCornersTransformation(radius))
|
||||
}
|
||||
binding.thumbnail.loadAny(item.manga)
|
||||
}
|
||||
}
|
||||
|
@@ -75,8 +75,14 @@ class SearchController(
|
||||
if (!isReplacingManga) {
|
||||
router.popController(this)
|
||||
if (newManga != null) {
|
||||
// Replaces old MangaController
|
||||
router.replaceTopController(RouterTransaction.with(MangaController(newManga)))
|
||||
val newMangaController = RouterTransaction.with(MangaController(newManga))
|
||||
if (router.backstack.last().controller is MangaController) {
|
||||
// Replace old MangaController
|
||||
router.replaceTopController(newMangaController)
|
||||
} else {
|
||||
// Push MangaController on top of MigrationController
|
||||
router.pushController(newMangaController)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -149,13 +149,10 @@ class SearchPresenter(
|
||||
db.updateMangaFavorite(prevManga).executeAsBlocking()
|
||||
}
|
||||
manga.favorite = true
|
||||
db.updateMangaFavorite(manga).executeAsBlocking()
|
||||
|
||||
// Update reading preferences
|
||||
manga.chapter_flags = prevManga.chapter_flags
|
||||
db.updateChapterFlags(manga).executeAsBlocking()
|
||||
manga.viewer_flags = prevManga.viewer_flags
|
||||
db.updateViewerFlags(manga).executeAsBlocking()
|
||||
|
||||
// Update date added
|
||||
if (replace) {
|
||||
@@ -165,8 +162,9 @@ class SearchPresenter(
|
||||
manga.date_added = Date().time
|
||||
}
|
||||
|
||||
// SearchPresenter#networkToLocalManga may have updated the manga title, so ensure db gets updated title
|
||||
db.updateMangaTitle(manga).executeAsBlocking()
|
||||
// SearchPresenter#networkToLocalManga may have updated the manga title,
|
||||
// so ensure db gets updated title too
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -37,8 +37,6 @@ class SourceSearchController(
|
||||
override fun onItemLongClick(position: Int) {
|
||||
view?.let { super.onItemClick(it, position) }
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val MANGA_KEY = "oldManga"
|
||||
}
|
||||
}
|
||||
|
||||
private const val MANGA_KEY = "oldManga"
|
||||
|
@@ -3,14 +3,14 @@ package eu.kanade.tachiyomi.ui.browse.migration.sources
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.databinding.SourceMainControllerCardItemBinding
|
||||
import eu.kanade.tachiyomi.databinding.SourceMainControllerItemBinding
|
||||
import eu.kanade.tachiyomi.source.icon
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
|
||||
class SourceHolder(view: View, val adapter: SourceAdapter) :
|
||||
FlexibleViewHolder(view, adapter) {
|
||||
|
||||
private val binding = SourceMainControllerCardItemBinding.bind(view)
|
||||
private val binding = SourceMainControllerItemBinding.bind(view)
|
||||
|
||||
fun bind(item: SourceItem) {
|
||||
val source = item.source
|
||||
|
@@ -21,7 +21,7 @@ data class SourceItem(val source: Source, val mangaCount: Int, val header: Selec
|
||||
* Returns the layout resource of this item.
|
||||
*/
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.source_main_controller_card_item
|
||||
return R.layout.source_main_controller_item
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -4,7 +4,7 @@ import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.databinding.SourceMainControllerCardItemBinding
|
||||
import eu.kanade.tachiyomi.databinding.SourceMainControllerItemBinding
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.icon
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
@@ -13,7 +13,7 @@ import eu.kanade.tachiyomi.util.view.setVectorCompat
|
||||
class SourceHolder(private val view: View, val adapter: SourceAdapter) :
|
||||
FlexibleViewHolder(view, adapter) {
|
||||
|
||||
private val binding = SourceMainControllerCardItemBinding.bind(view)
|
||||
private val binding = SourceMainControllerItemBinding.bind(view)
|
||||
|
||||
init {
|
||||
binding.sourceLatest.setOnClickListener {
|
||||
|
@@ -21,23 +21,14 @@ data class SourceItem(
|
||||
) :
|
||||
AbstractSectionableItem<SourceHolder, LangItem>(header) {
|
||||
|
||||
/**
|
||||
* Returns the layout resource of this item.
|
||||
*/
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.source_main_controller_card_item
|
||||
return R.layout.source_main_controller_item
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new view holder for this item.
|
||||
*/
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): SourceHolder {
|
||||
return SourceHolder(view, adapter as SourceAdapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds this item to the given view holder.
|
||||
*/
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: SourceHolder,
|
||||
|
@@ -49,6 +49,7 @@ import eu.kanade.tachiyomi.util.view.shrinkOnScroll
|
||||
import eu.kanade.tachiyomi.util.view.snack
|
||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
||||
import eu.kanade.tachiyomi.widget.EmptyView
|
||||
import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@@ -144,10 +145,9 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
filterSheet = SourceFilterSheet(
|
||||
activity!!,
|
||||
onFilterClicked = {
|
||||
val allDefault = presenter.sourceFilters == presenter.source.getFilterList()
|
||||
showProgressBar()
|
||||
adapter?.clear()
|
||||
presenter.setSourceFilter(if (allDefault) FilterList() else presenter.sourceFilters)
|
||||
presenter.setSourceFilter(presenter.sourceFilters)
|
||||
},
|
||||
onResetClicked = {
|
||||
presenter.appliedFilters = FilterList()
|
||||
@@ -332,7 +332,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
showProgressBar()
|
||||
adapter?.clear()
|
||||
|
||||
presenter.restartPager(newQuery)
|
||||
presenter.restartPager(newQuery, presenter.sourceFilters)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -627,8 +627,12 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
// Choose a category
|
||||
else -> {
|
||||
val ids = presenter.getMangaCategoryIds(manga)
|
||||
val preselected = ids.mapNotNull { id ->
|
||||
categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
|
||||
val preselected = categories.map {
|
||||
if (it.id in ids) {
|
||||
QuadStateTextView.State.CHECKED.ordinal
|
||||
} else {
|
||||
QuadStateTextView.State.UNCHECKED.ordinal
|
||||
}
|
||||
}.toTypedArray()
|
||||
|
||||
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
|
||||
@@ -644,11 +648,11 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
* @param mangas The list of manga to move to categories.
|
||||
* @param categories The list of categories where manga will be placed.
|
||||
*/
|
||||
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
|
||||
override fun updateCategoriesForMangas(mangas: List<Manga>, addCategories: List<Category>, removeCategories: List<Category>) {
|
||||
val manga = mangas.firstOrNull() ?: return
|
||||
|
||||
presenter.changeMangaFavorite(manga)
|
||||
presenter.updateMangaCategories(manga, categories)
|
||||
presenter.updateMangaCategories(manga, addCategories)
|
||||
|
||||
val position = adapter?.currentItems?.indexOfFirst { it -> (it as SourceItem).manga.id == manga.id }
|
||||
if (position != null) {
|
||||
|
@@ -38,7 +38,6 @@ import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import eu.kanade.tachiyomi.util.removeCovers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collect
|
||||
@@ -91,11 +90,6 @@ open class BrowseSourcePresenter(
|
||||
*/
|
||||
private lateinit var pager: Pager
|
||||
|
||||
/**
|
||||
* Flow of manga list to initialize.
|
||||
*/
|
||||
private val mangaDetailsFlow = MutableStateFlow<List<Manga>>(emptyList())
|
||||
|
||||
/**
|
||||
* Subscription for the pager.
|
||||
*/
|
||||
|
@@ -4,7 +4,6 @@ import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import coil.clear
|
||||
import coil.loadAny
|
||||
import coil.transform.RoundedCornersTransformation
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
||||
@@ -53,10 +52,8 @@ class SourceListHolder(private val view: View, adapter: FlexibleAdapter<*>) :
|
||||
override fun setImage(manga: Manga) {
|
||||
binding.thumbnail.clear()
|
||||
if (!manga.thumbnail_url.isNullOrEmpty()) {
|
||||
val radius = view.context.resources.getDimension(R.dimen.card_radius)
|
||||
binding.thumbnail.loadAny(manga) {
|
||||
setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false)
|
||||
transformations(RoundedCornersTransformation(radius))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.browse.source.filter
|
||||
import android.view.View
|
||||
import android.widget.CheckedTextView
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.R
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
@@ -72,7 +73,7 @@ open class TriStateItem(val filter: Filter.TriState) : AbstractFlexibleItem<TriS
|
||||
|
||||
init {
|
||||
// Align with native checkbox
|
||||
text.setPadding(4.dpToPx, 0, 0, 0)
|
||||
text.updatePadding(left = 4.dpToPx)
|
||||
text.compoundDrawablePadding = 20.dpToPx
|
||||
}
|
||||
}
|
||||
|
@@ -74,8 +74,6 @@ class GlobalSearchAdapter(val controller: GlobalSearchController) :
|
||||
interface OnTitleClickListener {
|
||||
fun onTitleClick(source: CatalogueSource)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val HOLDER_BUNDLE_KEY = "holder_bundle"
|
||||
}
|
||||
}
|
||||
|
||||
private const val HOLDER_BUNDLE_KEY = "holder_bundle"
|
||||
|
@@ -78,8 +78,6 @@ class CategoryRenameDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
|
||||
interface Listener {
|
||||
fun renameCategory(category: Category, name: String)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val CATEGORY_KEY = "CategoryRenameDialog.category"
|
||||
}
|
||||
}
|
||||
|
||||
private const val CATEGORY_KEY = "CategoryRenameDialog.category"
|
||||
|
@@ -10,6 +10,8 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.category.CategoryController
|
||||
import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView
|
||||
import eu.kanade.tachiyomi.widget.materialdialogs.setQuadStateMultiChoiceItems
|
||||
|
||||
class ChangeMangaCategoriesDialog<T>(bundle: Bundle? = null) :
|
||||
DialogController(bundle) where T : Controller, T : ChangeMangaCategoriesDialog.Listener {
|
||||
@@ -17,6 +19,7 @@ class ChangeMangaCategoriesDialog<T>(bundle: Bundle? = null) :
|
||||
private var mangas = emptyList<Manga>()
|
||||
private var categories = emptyList<Category>()
|
||||
private var preselected = emptyArray<Int>()
|
||||
private var selected = emptyArray<Int>().toIntArray()
|
||||
|
||||
constructor(
|
||||
target: T,
|
||||
@@ -27,6 +30,7 @@ class ChangeMangaCategoriesDialog<T>(bundle: Bundle? = null) :
|
||||
this.mangas = mangas
|
||||
this.categories = categories
|
||||
this.preselected = preselected
|
||||
this.selected = preselected.toIntArray()
|
||||
targetController = target
|
||||
}
|
||||
|
||||
@@ -36,15 +40,21 @@ class ChangeMangaCategoriesDialog<T>(bundle: Bundle? = null) :
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.apply {
|
||||
if (categories.isNotEmpty()) {
|
||||
val selected = categories
|
||||
.mapIndexed { i, _ -> preselected.contains(i) }
|
||||
.toBooleanArray()
|
||||
setMultiChoiceItems(categories.map { it.name }.toTypedArray(), selected) { _, which, checked ->
|
||||
selected[which] = checked
|
||||
setQuadStateMultiChoiceItems(
|
||||
items = categories.map { it.name },
|
||||
isActionList = false,
|
||||
initialSelected = preselected.toIntArray()
|
||||
) { selections ->
|
||||
selected = selections
|
||||
}
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val newCategories = categories.filterIndexed { i, _ -> selected[i] }
|
||||
(targetController as? Listener)?.updateCategoriesForMangas(mangas, newCategories)
|
||||
val add = selected
|
||||
.mapIndexed { index, value -> if (value == QuadStateTextView.State.CHECKED.ordinal) categories[index] else null }
|
||||
.filterNotNull()
|
||||
val remove = selected
|
||||
.mapIndexed { index, value -> if (value == QuadStateTextView.State.UNCHECKED.ordinal) categories[index] else null }
|
||||
.filterNotNull()
|
||||
(targetController as? Listener)?.updateCategoriesForMangas(mangas, add, remove)
|
||||
}
|
||||
} else {
|
||||
setMessage(R.string.information_empty_category_dialog)
|
||||
@@ -62,6 +72,6 @@ class ChangeMangaCategoriesDialog<T>(bundle: Bundle? = null) :
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>)
|
||||
fun updateCategoriesForMangas(mangas: List<Manga>, addCategories: List<Category>, removeCategories: List<Category> = emptyList<Category>())
|
||||
}
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import eu.kanade.tachiyomi.util.isLocal
|
||||
|
||||
class DeleteLibraryMangasDialog<T>(bundle: Bundle? = null) :
|
||||
DialogController(bundle) where T : Controller, T : DeleteLibraryMangasDialog.Listener {
|
||||
@@ -19,7 +20,17 @@ class DeleteLibraryMangasDialog<T>(bundle: Bundle? = null) :
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
val items = resources!!.getStringArray(R.array.delete_selected_mangas)
|
||||
val canDeleteChapters = mangas.any { !it.isLocal() }
|
||||
val items = when (canDeleteChapters) {
|
||||
true -> listOf(
|
||||
R.string.manga_from_library,
|
||||
R.string.downloaded_chapters,
|
||||
)
|
||||
false -> listOf(R.string.manga_from_library)
|
||||
}
|
||||
.map { resources!!.getString(it) }
|
||||
.toTypedArray()
|
||||
|
||||
val selected = items
|
||||
.mapIndexed { i, _ -> i == 0 }
|
||||
.toBooleanArray()
|
||||
@@ -30,7 +41,7 @@ class DeleteLibraryMangasDialog<T>(bundle: Bundle? = null) :
|
||||
}
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val deleteFromLibrary = selected[0]
|
||||
val deleteChapters = selected[1]
|
||||
val deleteChapters = canDeleteChapters && selected[1]
|
||||
(targetController as? Listener)?.deleteMangas(mangas, deleteFromLibrary, deleteChapters)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
|
@@ -4,11 +4,10 @@ import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.clear
|
||||
import coil.loadAny
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.databinding.SourceComfortableGridItemBinding
|
||||
import eu.kanade.tachiyomi.util.isLocal
|
||||
import eu.kanade.tachiyomi.util.view.loadAnyAutoPause
|
||||
|
||||
/**
|
||||
* Class used to hold the displayed data of a manga in the library, like the cover or the title.
|
||||
@@ -57,6 +56,6 @@ class LibraryComfortableGridHolder(
|
||||
|
||||
// Update the cover.
|
||||
binding.thumbnail.clear()
|
||||
binding.thumbnail.loadAny(item.manga)
|
||||
binding.thumbnail.loadAnyAutoPause(item.manga)
|
||||
}
|
||||
}
|
||||
|
@@ -3,10 +3,9 @@ package eu.kanade.tachiyomi.ui.library
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import coil.clear
|
||||
import coil.loadAny
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.databinding.SourceCompactGridItemBinding
|
||||
import eu.kanade.tachiyomi.util.isLocal
|
||||
import eu.kanade.tachiyomi.util.view.loadAnyAutoPause
|
||||
|
||||
/**
|
||||
* Class used to hold the displayed data of a manga in the library, like the cover or the title.
|
||||
@@ -55,6 +54,6 @@ open class LibraryCompactGridHolder(
|
||||
|
||||
// Update the cover.
|
||||
binding.thumbnail.clear()
|
||||
binding.thumbnail.loadAny(item.manga)
|
||||
binding.thumbnail.loadAnyAutoPause(item.manga)
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user