mirror of
https://github.com/mihonapp/mihon.git
synced 2025-07-30 03:15:54 +02:00
Compare commits
153 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 | |||
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 acknowledge that:
|
||||||
|
|
||||||
- I have updated:
|
- I have updated:
|
||||||
- To the latest version of the app (stable is v0.12.1)
|
- To the latest version of the app (stable is v0.12.2)
|
||||||
- All extensions
|
- All extensions
|
||||||
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
||||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||||
|
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]
|
labels: [Bug]
|
||||||
body:
|
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.12.1](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
|
|
||||||
|
|
||||||
- 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
|
- type: textarea
|
||||||
id: reproduce-steps
|
id: reproduce-steps
|
||||||
attributes:
|
attributes:
|
||||||
@ -98,9 +47,60 @@ body:
|
|||||||
placeholder: |
|
placeholder: |
|
||||||
You can paste the crash logs in pure text or upload it as an attachment.
|
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
|
- type: textarea
|
||||||
id: other-details
|
id: other-details
|
||||||
attributes:
|
attributes:
|
||||||
label: Other details
|
label: Other details
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Additional details and attachments.
|
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]
|
labels: [Feature request]
|
||||||
body:
|
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.12.1](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
|
||||||
required: true
|
|
||||||
- label: I will fill out all of the requested information in this form.
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: feature-description
|
id: feature-description
|
||||||
attributes:
|
attributes:
|
||||||
@ -37,3 +20,20 @@ body:
|
|||||||
label: Other details
|
label: Other details
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Additional details and attachments.
|
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
|
- master
|
||||||
tags:
|
tags:
|
||||||
- v*
|
- v*
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
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:
|
build:
|
||||||
name: Build app
|
name: Build app
|
||||||
needs: check_wrapper
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@ -33,6 +20,9 @@ jobs:
|
|||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Validate Gradle Wrapper
|
||||||
|
uses: gradle/wrapper-validation-action@v1
|
||||||
|
|
||||||
- name: Set up JDK 11
|
- name: Set up JDK 11
|
||||||
uses: actions/setup-java@v1
|
uses: actions/setup-java@v1
|
||||||
with:
|
with:
|
||||||
@ -44,10 +34,10 @@ jobs:
|
|||||||
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
||||||
|
|
||||||
- name: Build app
|
- name: Build app
|
||||||
uses: eskatos/gradle-command-action@v1
|
uses: gradle/gradle-command-action@v1
|
||||||
with:
|
with:
|
||||||
arguments: assembleStandardRelease
|
arguments: assembleStandardRelease
|
||||||
wrapper-cache-enabled: true
|
distributions-cache-enabled: true
|
||||||
dependencies-cache-enabled: true
|
dependencies-cache-enabled: true
|
||||||
configuration-cache-enabled: true
|
configuration-cache-enabled: true
|
||||||
|
|
||||||
@ -55,13 +45,10 @@ jobs:
|
|||||||
|
|
||||||
- name: Get tag name
|
- name: Get tag name
|
||||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
||||||
id: get_tag_name
|
|
||||||
run: |
|
run: |
|
||||||
set -x
|
set -x
|
||||||
echo "VERSION_TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
|
echo "VERSION_TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
|
||||||
|
|
||||||
# TODO: need to support multiple APKs
|
|
||||||
|
|
||||||
- name: Sign APK
|
- name: Sign APK
|
||||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
||||||
uses: r0adkll/sign-android-release@v1
|
uses: r0adkll/sign-android-release@v1
|
||||||
@ -75,9 +62,23 @@ jobs:
|
|||||||
- name: Clean up build artifacts
|
- name: Clean up build artifacts
|
||||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
||||||
run: |
|
run: |
|
||||||
cp ${{ env.SIGNED_RELEASE_FILE }} tachiyomi-${{ env.VERSION_TAG }}.apk
|
set -e
|
||||||
md5=`md5sum tachiyomi-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
|
|
||||||
echo "APK_MD5=$md5" >> $GITHUB_ENV
|
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
|
- name: Create Release
|
||||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
||||||
@ -86,9 +87,21 @@ jobs:
|
|||||||
tag_name: ${{ env.VERSION_TAG }}
|
tag_name: ${{ env.VERSION_TAG }}
|
||||||
name: Tachiyomi ${{ env.VERSION_TAG }}
|
name: Tachiyomi ${{ env.VERSION_TAG }}
|
||||||
body: |
|
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: |
|
files: |
|
||||||
tachiyomi-${{ env.VERSION_TAG }}.apk
|
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
|
draft: true
|
||||||
prerelease: false
|
prerelease: false
|
||||||
env:
|
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
|
## Our Pledge
|
||||||
|
|
||||||
In the interest of fostering an open and welcoming environment, we as
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
contributors and maintainers pledge to making participation in our project and
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
our community a harassment-free experience for everyone, regardless of age, body
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
level of experience, education, socio-economic status, nationality, personal
|
nationality, personal appearance, race, caste, color, religion, or sexual identity
|
||||||
appearance, race, religion, or sexual identity and orientation.
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
## Our Standards
|
## Our Standards
|
||||||
|
|
||||||
Examples of behavior that contributes to creating a positive environment
|
Examples of behavior that contributes to a positive environment for our
|
||||||
include:
|
community include:
|
||||||
|
|
||||||
* Using welcoming and inclusive language
|
* Demonstrating empathy and kindness toward other people
|
||||||
* Being respectful of differing viewpoints and experiences
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
* Gracefully accepting constructive criticism
|
* Giving and gracefully accepting constructive feedback
|
||||||
* Focusing on what is best for the community
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
* Showing empathy towards other community members
|
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
|
* The use of sexualized language or imagery, and sexual attention or
|
||||||
advances
|
advances of any kind
|
||||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
* Public or private harassment
|
* Public or private harassment
|
||||||
* Publishing others' private information, such as a physical or electronic
|
* Publishing others' private information, such as a physical or email
|
||||||
address, without explicit permission
|
address, without their explicit permission
|
||||||
* Other conduct which could reasonably be considered inappropriate in a
|
* 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
|
Community moderators are responsible for clarifying and enforcing our standards of
|
||||||
behavior and are expected to take appropriate and fair corrective action in
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
response to any instances of unacceptable behavior.
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
Project maintainers have the right and responsibility to remove, edit, or
|
Community moderators have the right and responsibility to remove, edit, or reject
|
||||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
permanently any contributor for other behaviors that they deem inappropriate,
|
decisions when appropriate.
|
||||||
threatening, offensive, or harmful.
|
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
This Code of Conduct applies both within project spaces and in public spaces
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
when an individual is representing the project or its community. Examples of
|
an individual is officially representing the community in public spaces.
|
||||||
representing a project or community include using an official project e-mail
|
Examples of representing our community include using an official e-mail address,
|
||||||
address, posting via an official social media account, or acting as an appointed
|
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
|
representative at an online or offline event.
|
||||||
further defined and clarified by project maintainers.
|
|
||||||
|
|
||||||
## Enforcement
|
## Enforcement
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
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
|
reported to the community moderators responsible for enforcement at
|
||||||
complaints will be reviewed and investigated and will result in a response that
|
the [Tachiyomi Discord server](https://discord.gg/tachiyomi).
|
||||||
is deemed necessary and appropriate to the circumstances. The project team is
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
|
||||||
Further details of specific enforcement policies may be posted separately.
|
|
||||||
|
|
||||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
All community moderators are obligated to respect the privacy and security of the
|
||||||
faith may face temporary or permanent repercussions as determined by other
|
reporter of any incident.
|
||||||
members of the project's leadership.
|
|
||||||
|
## 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
|
## Attribution
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/),
|
||||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
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
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
https://www.contributor-covenant.org/faq
|
[FAQ](https://www.contributor-covenant.org/faq). Translations are available
|
||||||
|
at [translations](https://www.contributor-covenant.org/translations).
|
||||||
|
@ -6,8 +6,6 @@
|
|||||||
# Tachiyomi
|
# Tachiyomi
|
||||||
Tachiyomi is a free and open source manga reader for Android 6.0 and above.
|
Tachiyomi is a free and open source manga reader for Android 6.0 and above.
|
||||||
|
|
||||||

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