mirror of
https://github.com/mihonapp/mihon.git
synced 2025-07-30 11:25:54 +02:00
Compare commits
202 Commits
Author | SHA1 | Date | |
---|---|---|---|
e863e8c64b | |||
f5b591430c | |||
8cfaf8eb51 | |||
675c0cefc3 | |||
1a52385b78 | |||
372e500590 | |||
cc1a317439 | |||
6d650518a1 | |||
7940117577 | |||
b0f87fdd21 | |||
dc92ffed87 | |||
4af578e310 | |||
e22825d818 | |||
e2da6259e7 | |||
d149017c60 | |||
afc400121b | |||
ef993515c6 | |||
edb1d21ddc | |||
ba8abd94a8 | |||
c6d4e4c15f | |||
09f0ac866f | |||
7ed25704d6 | |||
2196dac63e | |||
c8f70efded | |||
ea97488670 | |||
c2255b0a0f | |||
f754b081ce | |||
07771cb5e4 | |||
690d8e43ae | |||
82f14a7d59 | |||
b284384f0a | |||
1ae0d1b5d0 | |||
9de08c8166 | |||
a2d007f2a9 | |||
774f818bbb | |||
0ec7121b8f | |||
d7d46f4447 | |||
45fad147bf | |||
3664195c71 | |||
fce3cd00a1 | |||
33b3be0d0e | |||
cfd1b4a6c6 | |||
d45fefd6f0 | |||
f125ab01ee | |||
be001d090c | |||
971d8a7e40 | |||
a2cf210a52 | |||
3eec207166 | |||
b5d83bdb56 | |||
2c495c4119 | |||
7c72d6cb7c | |||
8362bf0886 | |||
1a8155c45b | |||
3f2f946019 | |||
2c14a8dee1 | |||
917a283bd1 | |||
3e403d5ab3 | |||
746d35b52b | |||
9a7a03e327 | |||
a051079c6a | |||
7b3c18bb97 | |||
52daf3d58c | |||
f41bde5ee1 | |||
6151318ac1 | |||
b45c322729 | |||
b00e8768dc | |||
156feb6e8e | |||
e942b8a402 | |||
abdb67a123 | |||
ee20787c5e | |||
ec4e631760 | |||
02b430a5bf | |||
7878053df2 | |||
12a593c3c6 | |||
6b1f130750 | |||
bde4c0a648 | |||
5ae4621da1 | |||
5ea8d0546e | |||
8a064c118f | |||
2f91c27df2 | |||
763bd54707 | |||
0ea3cc7ce4 | |||
0de3558ab3 | |||
069f4e12d8 | |||
ae4dfc9956 | |||
ee711dc0fb | |||
c316e7faab | |||
7083b3d912 | |||
2d3a1b6a9e | |||
0df23ab878 | |||
7ed8de2ef4 | |||
d935e22f0d | |||
0e26abf7a6 | |||
59aef13200 | |||
9d1f6c4416 | |||
b9f7660a91 | |||
18b5250ed1 | |||
f683f21ee2 | |||
bd033db84c | |||
ab036312a4 | |||
634da15191 | |||
cea1720ea0 | |||
3f2f542265 | |||
b77edb2b5b | |||
1b699bb814 | |||
333c035fed | |||
ce29914c56 | |||
70e5361146 | |||
e7d6dfff53 | |||
eebfad5a95 | |||
77c0a93ac6 | |||
63a3e126b3 | |||
3ea84cf0ce | |||
7fa80ae556 | |||
925f71af15 | |||
c666dd623d | |||
2cd8733212 | |||
4b2a9bc621 | |||
12a9d0575d | |||
edcfa28b0b | |||
3155829994 | |||
d25707554e | |||
38df44ef4b | |||
df683375b1 | |||
cc3cbbc4bb | |||
6922394b8e | |||
24fd82d773 | |||
57aefcd917 | |||
b3854ad382 | |||
5f5fc77877 | |||
0493e77cff | |||
6240fe1dfc | |||
beb7f90908 | |||
a3917972b4 | |||
7094fef37f | |||
0f41e56a24 | |||
52b283283f | |||
ebb15bf96c | |||
6c527d52fb | |||
b8ea57e097 | |||
909aed4262 | |||
4d2fff9538 | |||
9a45983f17 | |||
11926014da | |||
72002c13d6 | |||
6ed767ae84 | |||
3826b307f7 | |||
887b157056 | |||
d36dd39743 | |||
dd008bc13a | |||
50b282f58b | |||
f8a7efbce7 | |||
7d2caeb270 | |||
708e71a35a | |||
4eaccc966e | |||
3670d649b8 | |||
90ab04e81d | |||
26b8df5354 | |||
11a8046c5f | |||
da16110e1c | |||
914b686c8e | |||
27133520fc | |||
24b967ad5c | |||
ca4b4a3f1e | |||
faef35ec47 | |||
326d4c2641 | |||
83436c9550 | |||
2084822731 | |||
071bad1232 | |||
ae1a76da2b | |||
fbc6965c4e | |||
57a5862840 | |||
91fbccdbaa | |||
0ab0dd95ae | |||
bc41040fd3 | |||
4c8dfd0c0c | |||
2b9dbfb390 | |||
84d546b724 | |||
63053b9940 | |||
2256030a2a | |||
79da33b597 | |||
7d67450e58 | |||
8aa11951bf | |||
f23f22ab01 | |||
96a64c7bd2 | |||
d1bb0fdf1d | |||
feca30d7ed | |||
b650151693 | |||
bb3afd0dc9 | |||
5e77ae208d | |||
24e5a4d7ec | |||
1d10d29fa9 | |||
9b00e91773 | |||
cd73c30d6f | |||
7bbba0c7d9 | |||
7907a4fc24 | |||
2f94f62a56 | |||
85791a9336 | |||
a4eba50cfd | |||
03980b2f27 | |||
664e5cfb59 | |||
b9736df7e0 |
.github
CODE_OF_CONDUCT.mdREADME.mdapp
build.gradle.kts
build.gradle.ktssrc
main
AndroidManifest.xml
java
com
google
android
material
eu
kanade
tachiyomi
App.ktAppModule.ktMigrations.kt
annotations
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
ExtensionAdapter.ktExtensionController.ktExtensionHolder.ktExtensionItem.ktExtensionPresenter.ktExtensionTrustDialog.kt
details
migration
manga
search
sources
source
category
library
ChangeMangaCategoriesDialog.ktDeleteLibraryMangasDialog.ktLibraryAdapter.ktLibraryCategoryView.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.ktReaderSlider.kt
setting
viewer
recent
security
setting
SettingsAdvancedController.ktSettingsAppearanceController.ktSettingsBackupController.ktSettingsBrowseController.ktSettingsController.ktSettingsDownloadController.ktSettingsGeneralController.ktSettingsLibraryController.ktSettingsMainController.ktSettingsReaderController.ktSettingsSecurityController.ktSettingsTrackingController.kt
search
util
chapter
lang
preference
storage
system
AnimationExtensions.ktAuthenticatorUtil.ktContextExtensions.ktGLUtil.ktImageUtil.ktLocaleHelper.ktNotificationExtensions.kt
view
widget
ActionToolbar.ktElevationAppBarLayout.ktEmptyView.ktExtendedNavigationView.ktHideBottomNavigationOnScrollBehavior.ktMaterialFastScroll.ktMaterialSpinnerView.ktNegativeSeekBar.ktRecyclerViewPagerAdapter.ktSimpleNavigationView.ktTachiyomiBottomNavigationView.ktTachiyomiChangeHandlerFrameLayout.ktTachiyomiCoordinatorLayout.ktTachiyomiFullscreenDialog.ktTachiyomiScrollingViewBehavior.ktTachiyomiSearchView.ktTachiyomiTextInputEditText.ktThemedSwipeRefreshLayout.kt
listener
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
font
layout-sw720dp
layout
action_toolbar.xmlcategories_item.xmlchapters_item.xmlcommon_dialog_with_checkbox.xmlcommon_view_empty.xmldialog_stub_textinput.xmldownload_custom_amount.xmldownload_item.xmlextension_card_item.xmlextension_detail_header.xmlglobal_search_controller_card.xmlglobal_search_controller_card_item.xmlhistory_item.xmllicenses_controller.xmllicenses_item.xmlmain_activity.xmlmanga_chapters_header.xmlmanga_full_cover_dialog.xmlmanga_info_header.xmlmigration_sources_controller.xmlnavigation_view_checkbox.xmlnavigation_view_checkedtext.xmlnavigation_view_group.xmlnavigation_view_radio.xmlnavigation_view_spinner.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_pager_settings.xmlreader_reading_mode_settings.xmlreader_transition_view.xmlreader_webtoon_settings.xmlsection_header_item.xmlsettings_search_controller_card.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_migrate.xmlbrowse_sources.xmlextension_details.xmlfull_cover.xmlglobal_search.xmlhistory.xmllibrary.xmlmanga.xmlsettings_main.xmlsource_browse.xmltrack_search.xmlupdates_chapter_selection.xml
values-aii
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-eu
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-lt
values-lv
values-mr
values-ms
values-my
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.11.1)
|
- To the latest version of the app (stable is v0.12.3)
|
||||||
- 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
|
||||||
|
104
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
104
.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.11.1](https://github.com/tachiyomiorg/tachiyomi/releases/tag/v0.11.1)**.
|
|
||||||
required: true
|
|
||||||
- label: I have updated all installed extensions.
|
|
||||||
required: true
|
|
||||||
- label: I will fill out all of the requested information in this form.
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: tachiyomi-version
|
|
||||||
attributes:
|
|
||||||
label: Tachiyomi version
|
|
||||||
description: You can find your Tachiyomi version in **More → About**.
|
|
||||||
placeholder: |
|
|
||||||
Example: "0.11.1"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: android-version
|
|
||||||
attributes:
|
|
||||||
label: Android version
|
|
||||||
description: You can find this somewhere in your Android settings.
|
|
||||||
placeholder: |
|
|
||||||
Example: "Android 11"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: device
|
|
||||||
attributes:
|
|
||||||
label: Device
|
|
||||||
description: List your device and model.
|
|
||||||
placeholder: |
|
|
||||||
Example: "Google Pixel 5"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: reproduce-steps
|
id: reproduce-steps
|
||||||
attributes:
|
attributes:
|
||||||
@ -84,7 +33,7 @@ body:
|
|||||||
label: Actual behavior
|
label: Actual behavior
|
||||||
description: Explain what actually happens.
|
description: Explain what actually happens.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Example:
|
Example:
|
||||||
"This happened instead..."
|
"This happened instead..."
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
@ -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.3"
|
||||||
|
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.3](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.11.1](https://github.com/tachiyomiorg/tachiyomi/releases/tag/v0.11.1)**.
|
|
||||||
required: true
|
|
||||||
- label: I will fill out all of the requested information in this form.
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
- 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.3](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 }}
|
2
.github/workflows/issue_closer.yml
vendored
2
.github/workflows/issue_closer.yml
vendored
@ -25,7 +25,7 @@ jobs:
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "both",
|
"type": "both",
|
||||||
"regex": ".*(aniyomi|anime).*",
|
"regex": "^(?!.*myanimelist.*).*(aniyomi|anime).*$",
|
||||||
"ignoreCase": true,
|
"ignoreCase": true,
|
||||||
"message": "Tachiyomi does not support anime, and has no plans to support anime. In addition Tachiyomi is not affiliated with Aniyomi https://github.com/jmir1/aniyomi"
|
"message": "Tachiyomi does not support anime, and has no plans to support anime. In addition Tachiyomi is not affiliated with Aniyomi https://github.com/jmir1/aniyomi"
|
||||||
}
|
}
|
||||||
|
@ -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 = 66
|
versionCode = 69
|
||||||
versionName = "0.12.0"
|
versionName = "0.12.3"
|
||||||
|
|
||||||
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")
|
||||||
|
|
||||||
@ -137,37 +138,31 @@ dependencies {
|
|||||||
implementation("org.tachiyomi:source-api:1.1")
|
implementation("org.tachiyomi:source-api:1.1")
|
||||||
|
|
||||||
// AndroidX libraries
|
// AndroidX libraries
|
||||||
implementation("androidx.annotation:annotation:1.3.0-alpha01")
|
implementation("androidx.annotation:annotation:1.3.0-beta01")
|
||||||
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.4.0-beta01")
|
||||||
implementation("androidx.cardview:cardview:1.0.0")
|
implementation("androidx.constraintlayout:constraintlayout:2.1.1")
|
||||||
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-beta02")
|
||||||
implementation("androidx.core:core-splashscreen:1.0.0-alpha01")
|
implementation("androidx.core:core-splashscreen:1.0.0-alpha02")
|
||||||
implementation("androidx.preference:preference-ktx:1.1.1")
|
implementation("androidx.recyclerview:recyclerview:1.3.0-alpha01")
|
||||||
implementation("androidx.recyclerview:recyclerview:1.2.1")
|
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
||||||
|
implementation("androidx.viewpager:viewpager:1.1.0-alpha01")
|
||||||
|
|
||||||
val lifecycleVersion = "2.4.0-alpha01"
|
val lifecycleVersion = "2.4.0-beta01"
|
||||||
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
|
implementation("androidx.lifecycle:lifecycle-common:$lifecycleVersion")
|
||||||
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
|
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
|
||||||
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 +174,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"
|
||||||
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 +201,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 +212,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,22 +222,19 @@ 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-alpha04")
|
||||||
implementation("com.github.dmytrodanylyk.android-process-button:library:1.0.4")
|
implementation("com.github.dmytrodanylyk.android-process-button:library:1.0.4")
|
||||||
implementation("eu.davidea:flexible-adapter:5.1.0")
|
implementation("eu.davidea:flexible-adapter:5.1.0")
|
||||||
implementation("eu.davidea:flexible-adapter-ui:1.0.0")
|
implementation("eu.davidea:flexible-adapter-ui:1.0.0")
|
||||||
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
|
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
|
||||||
implementation("com.github.chrisbanes:PhotoView:2.3.0")
|
implementation("com.github.chrisbanes:PhotoView:2.3.0")
|
||||||
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0")
|
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0") {
|
||||||
|
exclude(group = "androidx.viewpager", module = "viewpager")
|
||||||
|
}
|
||||||
implementation("dev.chrisbanes.insetter:insetter:0.6.0")
|
implementation("dev.chrisbanes.insetter:insetter:0.6.0")
|
||||||
|
|
||||||
// Conductor
|
// Conductor
|
||||||
@ -256,8 +251,20 @@ 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}")
|
||||||
|
|
||||||
|
// Shizuku
|
||||||
|
val shizukuVersion = "12.0.0"
|
||||||
|
implementation("dev.rikka.shizuku:api:$shizukuVersion")
|
||||||
|
implementation("dev.rikka.shizuku:provider:$shizukuVersion")
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
testImplementation("junit:junit:4.13.2")
|
testImplementation("junit:junit:4.13.2")
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
<!-- For managing extensions -->
|
<!-- For managing extensions -->
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||||
|
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
||||||
<!-- To view extension packages in API 30+ -->
|
<!-- To view extension packages in API 30+ -->
|
||||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||||
|
|
||||||
@ -188,6 +189,9 @@
|
|||||||
android:name=".data.backup.BackupRestoreService"
|
android:name=".data.backup.BackupRestoreService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
|
<service android:name=".extension.util.ExtensionInstallService"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="${applicationId}.provider"
|
android:authorities="${applicationId}.provider"
|
||||||
@ -198,6 +202,19 @@
|
|||||||
android:resource="@xml/provider_paths" />
|
android:resource="@xml/provider_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="rikka.shizuku.ShizukuProvider"
|
||||||
|
android:authorities="${applicationId}.shizuku"
|
||||||
|
android:multiprocess="false"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="true"
|
||||||
|
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
||||||
|
|
||||||
|
<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>
|
||||||
|
@ -0,0 +1,100 @@
|
|||||||
|
package com.google.android.material.appbar
|
||||||
|
|
||||||
|
import android.animation.ValueAnimator
|
||||||
|
import android.view.View
|
||||||
|
import android.view.animation.DecelerateInterpolator
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.core.animation.doOnEnd
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.marginTop
|
||||||
|
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
||||||
|
import eu.kanade.tachiyomi.util.view.findChild
|
||||||
|
import eu.kanade.tachiyomi.widget.ElevationAppBarLayout
|
||||||
|
import kotlin.math.roundToLong
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide toolbar on scroll behavior for [AppBarLayout].
|
||||||
|
*
|
||||||
|
* Inside this package to access some package-private methods.
|
||||||
|
*/
|
||||||
|
class HideToolbarOnScrollBehavior : AppBarLayout.Behavior() {
|
||||||
|
|
||||||
|
@ViewCompat.NestedScrollType
|
||||||
|
private var lastStartedType: Int = 0
|
||||||
|
|
||||||
|
private var offsetAnimator: ValueAnimator? = null
|
||||||
|
|
||||||
|
private var toolbarHeight: Int = 0
|
||||||
|
|
||||||
|
override fun onStartNestedScroll(
|
||||||
|
parent: CoordinatorLayout,
|
||||||
|
child: AppBarLayout,
|
||||||
|
directTargetChild: View,
|
||||||
|
target: View,
|
||||||
|
nestedScrollAxes: Int,
|
||||||
|
type: Int
|
||||||
|
): Boolean {
|
||||||
|
lastStartedType = type
|
||||||
|
offsetAnimator?.cancel()
|
||||||
|
return super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes, type)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopNestedScroll(
|
||||||
|
parent: CoordinatorLayout,
|
||||||
|
layout: AppBarLayout,
|
||||||
|
target: View,
|
||||||
|
type: Int
|
||||||
|
) {
|
||||||
|
super.onStopNestedScroll(parent, layout, target, type)
|
||||||
|
if (toolbarHeight == 0) {
|
||||||
|
toolbarHeight = layout.findChild<Toolbar>()?.height ?: 0
|
||||||
|
}
|
||||||
|
if (lastStartedType == ViewCompat.TYPE_TOUCH || type == ViewCompat.TYPE_NON_TOUCH) {
|
||||||
|
animateToolbarVisibility(
|
||||||
|
parent,
|
||||||
|
layout,
|
||||||
|
getTopBottomOffsetForScrollingSibling(layout) > -toolbarHeight / 2
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFlingFinished(parent: CoordinatorLayout, layout: AppBarLayout) {
|
||||||
|
super.onFlingFinished(parent, layout)
|
||||||
|
animateToolbarVisibility(
|
||||||
|
parent,
|
||||||
|
layout,
|
||||||
|
getTopBottomOffsetForScrollingSibling(layout) > -toolbarHeight / 2
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getTopBottomOffsetForScrollingSibling(abl: AppBarLayout): Int {
|
||||||
|
return topBottomOffsetForScrollingSibling - abl.marginTop
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun animateToolbarVisibility(
|
||||||
|
coordinatorLayout: CoordinatorLayout,
|
||||||
|
child: AppBarLayout,
|
||||||
|
isVisible: Boolean
|
||||||
|
) {
|
||||||
|
val current = getTopBottomOffsetForScrollingSibling(child)
|
||||||
|
val target = if (isVisible) 0 else -toolbarHeight
|
||||||
|
if (current == target) return
|
||||||
|
|
||||||
|
offsetAnimator?.cancel()
|
||||||
|
offsetAnimator = ValueAnimator().apply {
|
||||||
|
interpolator = DecelerateInterpolator()
|
||||||
|
duration = (150 * child.context.animatorDurationScale).roundToLong()
|
||||||
|
addUpdateListener {
|
||||||
|
setHeaderTopBottomOffset(coordinatorLayout, child, it.animatedValue as Int)
|
||||||
|
}
|
||||||
|
doOnEnd {
|
||||||
|
if ((child as? ElevationAppBarLayout)?.isTransparentWhenNotLifted == true) {
|
||||||
|
child.isLifted = !isVisible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setIntValues(current, target)
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -12,9 +12,8 @@ import android.webkit.WebView
|
|||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleObserver
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.OnLifecycleEvent
|
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
@ -30,6 +29,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
|
||||||
@ -43,14 +44,14 @@ import uy.kohesive.injekt.api.get
|
|||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.security.Security
|
import java.security.Security
|
||||||
|
|
||||||
open class App : Application(), LifecycleObserver, ImageLoaderFactory {
|
open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
||||||
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
private val disableIncognitoReceiver = DisableIncognitoReceiver()
|
private val disableIncognitoReceiver = DisableIncognitoReceiver()
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super<Application>.onCreate()
|
||||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||||
|
|
||||||
// TLS 1.3 support for Android < 10
|
// TLS 1.3 support for Android < 10
|
||||||
@ -114,25 +115,23 @@ 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
|
override fun onStop(owner: LifecycleOwner) {
|
||||||
@Suppress("unused")
|
if (!AuthenticatorUtil.isAuthenticating && preferences.lockAppAfter().get() >= 0) {
|
||||||
fun onAppBackgrounded() {
|
|
||||||
if (preferences.lockAppAfter().get() >= 0) {
|
|
||||||
SecureActivityDelegate.locked = true
|
SecureActivityDelegate.locked = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -176,8 +175,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.annotations
|
package eu.kanade.tachiyomi.annotations
|
||||||
|
|
||||||
|
// TODO: remove this when no longer used in extensions
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
@Target(AnnotationTarget.CLASS)
|
@Target(AnnotationTarget.CLASS)
|
||||||
annotation class Nsfw
|
annotation class Nsfw
|
||||||
|
@ -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 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,8 @@ interface Manga : SManga {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getGenres(): List<String>? {
|
fun getGenres(): List<String>? {
|
||||||
return genre?.split(", ")?.map { it.trim() }
|
if (genre.isNullOrBlank()) return null
|
||||||
|
return genre?.split(", ")?.map { it.trim() }?.filterNot { it.isBlank() }?.distinct()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setChapterFlags(flag: Int, mask: Int) {
|
private fun setChapterFlags(flag: Int, mask: Int) {
|
||||||
|
@ -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,27 +4,32 @@ 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.isConnectedToWifi
|
||||||
|
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 kotlinx.coroutines.CoroutineScope
|
||||||
import rx.schedulers.Schedulers
|
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 timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -80,16 +85,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 +106,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 +134,43 @@ 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 { error ->
|
||||||
|
withUIContext {
|
||||||
|
Timber.e(error)
|
||||||
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() && !isConnectedToWifi()) {
|
||||||
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.
|
||||||
*/
|
*/
|
||||||
|
@ -51,7 +51,7 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
* Cached progress notification to avoid creating a lot.
|
* Cached progress notification to avoid creating a lot.
|
||||||
*/
|
*/
|
||||||
val progressNotificationBuilder by lazy {
|
val progressNotificationBuilder by lazy {
|
||||||
context.notificationBuilder(Notifications.CHANNEL_LIBRARY) {
|
context.notificationBuilder(Notifications.CHANNEL_LIBRARY_PROGRESS) {
|
||||||
setContentTitle(context.getString(R.string.app_name))
|
setContentTitle(context.getString(R.string.app_name))
|
||||||
setSmallIcon(R.drawable.ic_refresh_24dp)
|
setSmallIcon(R.drawable.ic_refresh_24dp)
|
||||||
setLargeIcon(notificationBitmap)
|
setLargeIcon(notificationBitmap)
|
||||||
@ -101,7 +101,7 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
|
|
||||||
context.notificationManager.notify(
|
context.notificationManager.notify(
|
||||||
Notifications.ID_LIBRARY_ERROR,
|
Notifications.ID_LIBRARY_ERROR,
|
||||||
context.notificationBuilder(Notifications.CHANNEL_LIBRARY) {
|
context.notificationBuilder(Notifications.CHANNEL_LIBRARY_ERROR) {
|
||||||
setContentTitle(context.resources.getQuantityString(R.plurals.notification_update_error, errors.size, errors.size))
|
setContentTitle(context.resources.getQuantityString(R.plurals.notification_update_error, errors.size, errors.size))
|
||||||
setStyle(
|
setStyle(
|
||||||
NotificationCompat.BigTextStyle().bigText(
|
NotificationCompat.BigTextStyle().bigText(
|
||||||
|
@ -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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -352,7 +350,7 @@ class LibraryUpdateService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preferences.showLibraryUpdateErrors() && failedUpdates.isNotEmpty()) {
|
if (failedUpdates.isNotEmpty()) {
|
||||||
val errorFile = writeErrorFile(failedUpdates)
|
val errorFile = writeErrorFile(failedUpdates)
|
||||||
notifier.showUpdateErrorNotification(
|
notifier.showUpdateErrorNotification(
|
||||||
failedUpdates.map { it.first.title },
|
failedUpdates.map { it.first.title },
|
||||||
@ -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.
|
||||||
@ -23,8 +24,10 @@ object Notifications {
|
|||||||
/**
|
/**
|
||||||
* Notification channel and ids used by the library updater.
|
* Notification channel and ids used by the library updater.
|
||||||
*/
|
*/
|
||||||
const val CHANNEL_LIBRARY = "library_channel"
|
private const val GROUP_LIBRARY = "group_library"
|
||||||
|
const val CHANNEL_LIBRARY_PROGRESS = "library_progress_channel"
|
||||||
const val ID_LIBRARY_PROGRESS = -101
|
const val ID_LIBRARY_PROGRESS = -101
|
||||||
|
const val CHANNEL_LIBRARY_ERROR = "library_errors_channel"
|
||||||
const val ID_LIBRARY_ERROR = -102
|
const val ID_LIBRARY_ERROR = -102
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -50,6 +53,7 @@ object Notifications {
|
|||||||
*/
|
*/
|
||||||
const val CHANNEL_UPDATES_TO_EXTS = "updates_ext_channel"
|
const val CHANNEL_UPDATES_TO_EXTS = "updates_ext_channel"
|
||||||
const val ID_UPDATES_TO_EXTS = -401
|
const val ID_UPDATES_TO_EXTS = -401
|
||||||
|
const val ID_EXTENSION_INSTALLER = -402
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notification channel and ids used by the backup/restore system.
|
* Notification channel and ids used by the backup/restore system.
|
||||||
@ -76,99 +80,90 @@ object Notifications {
|
|||||||
|
|
||||||
private val deprecatedChannels = listOf(
|
private val deprecatedChannels = listOf(
|
||||||
"downloader_channel",
|
"downloader_channel",
|
||||||
"backup_restore_complete_channel"
|
"backup_restore_complete_channel",
|
||||||
|
"library_channel",
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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(
|
notificationService.createNotificationChannelGroupsCompat(
|
||||||
NotificationChannelGroup(GROUP_BACKUP_RESTORE, context.getString(R.string.group_backup_restore)),
|
listOf(
|
||||||
NotificationChannelGroup(GROUP_DOWNLOADER, context.getString(R.string.group_downloader))
|
buildNotificationChannelGroup(GROUP_BACKUP_RESTORE) {
|
||||||
).forEach(context.notificationManager::createNotificationChannelGroup)
|
setName(context.getString(R.string.label_backup))
|
||||||
|
},
|
||||||
listOf(
|
buildNotificationChannelGroup(GROUP_DOWNLOADER) {
|
||||||
NotificationChannel(
|
setName(context.getString(R.string.download_notifier_downloader_title))
|
||||||
CHANNEL_COMMON,
|
},
|
||||||
context.getString(R.string.channel_common),
|
buildNotificationChannelGroup(GROUP_LIBRARY) {
|
||||||
NotificationManager.IMPORTANCE_LOW
|
setName(context.getString(R.string.label_library))
|
||||||
),
|
},
|
||||||
NotificationChannel(
|
|
||||||
CHANNEL_LIBRARY,
|
|
||||||
context.getString(R.string.channel_library),
|
|
||||||
NotificationManager.IMPORTANCE_LOW
|
|
||||||
).apply {
|
|
||||||
setShowBadge(false)
|
|
||||||
},
|
|
||||||
NotificationChannel(
|
|
||||||
CHANNEL_DOWNLOADER_PROGRESS,
|
|
||||||
context.getString(R.string.channel_progress),
|
|
||||||
NotificationManager.IMPORTANCE_LOW
|
|
||||||
).apply {
|
|
||||||
group = GROUP_DOWNLOADER
|
|
||||||
setShowBadge(false)
|
|
||||||
},
|
|
||||||
NotificationChannel(
|
|
||||||
CHANNEL_DOWNLOADER_COMPLETE,
|
|
||||||
context.getString(R.string.channel_complete),
|
|
||||||
NotificationManager.IMPORTANCE_LOW
|
|
||||||
).apply {
|
|
||||||
group = GROUP_DOWNLOADER
|
|
||||||
setShowBadge(false)
|
|
||||||
},
|
|
||||||
NotificationChannel(
|
|
||||||
CHANNEL_DOWNLOADER_ERROR,
|
|
||||||
context.getString(R.string.channel_errors),
|
|
||||||
NotificationManager.IMPORTANCE_LOW
|
|
||||||
).apply {
|
|
||||||
group = GROUP_DOWNLOADER
|
|
||||||
setShowBadge(false)
|
|
||||||
},
|
|
||||||
NotificationChannel(
|
|
||||||
CHANNEL_NEW_CHAPTERS,
|
|
||||||
context.getString(R.string.channel_new_chapters),
|
|
||||||
NotificationManager.IMPORTANCE_DEFAULT
|
|
||||||
),
|
|
||||||
NotificationChannel(
|
|
||||||
CHANNEL_UPDATES_TO_EXTS,
|
|
||||||
context.getString(R.string.channel_ext_updates),
|
|
||||||
NotificationManager.IMPORTANCE_DEFAULT
|
|
||||||
),
|
|
||||||
NotificationChannel(
|
|
||||||
CHANNEL_BACKUP_RESTORE_PROGRESS,
|
|
||||||
context.getString(R.string.channel_progress),
|
|
||||||
NotificationManager.IMPORTANCE_LOW
|
|
||||||
).apply {
|
|
||||||
group = GROUP_BACKUP_RESTORE
|
|
||||||
setShowBadge(false)
|
|
||||||
},
|
|
||||||
NotificationChannel(
|
|
||||||
CHANNEL_BACKUP_RESTORE_COMPLETE,
|
|
||||||
context.getString(R.string.channel_complete),
|
|
||||||
NotificationManager.IMPORTANCE_HIGH
|
|
||||||
).apply {
|
|
||||||
group = GROUP_BACKUP_RESTORE
|
|
||||||
setShowBadge(false)
|
|
||||||
setSound(null, null)
|
|
||||||
},
|
|
||||||
NotificationChannel(
|
|
||||||
CHANNEL_CRASH_LOGS,
|
|
||||||
context.getString(R.string.channel_crash_logs),
|
|
||||||
NotificationManager.IMPORTANCE_HIGH
|
|
||||||
),
|
|
||||||
NotificationChannel(
|
|
||||||
CHANNEL_INCOGNITO_MODE,
|
|
||||||
context.getString(R.string.pref_incognito_mode),
|
|
||||||
NotificationManager.IMPORTANCE_LOW
|
|
||||||
)
|
)
|
||||||
).forEach(context.notificationManager::createNotificationChannel)
|
)
|
||||||
|
|
||||||
|
notificationService.createNotificationChannelsCompat(
|
||||||
|
listOf(
|
||||||
|
buildNotificationChannel(CHANNEL_COMMON, IMPORTANCE_LOW) {
|
||||||
|
setName(context.getString(R.string.channel_common))
|
||||||
|
},
|
||||||
|
buildNotificationChannel(CHANNEL_LIBRARY_PROGRESS, IMPORTANCE_LOW) {
|
||||||
|
setName(context.getString(R.string.channel_progress))
|
||||||
|
setGroup(GROUP_LIBRARY)
|
||||||
|
setShowBadge(false)
|
||||||
|
},
|
||||||
|
buildNotificationChannel(CHANNEL_LIBRARY_ERROR, IMPORTANCE_LOW) {
|
||||||
|
setName(context.getString(R.string.channel_errors))
|
||||||
|
setGroup(GROUP_LIBRARY)
|
||||||
|
setShowBadge(false)
|
||||||
|
},
|
||||||
|
buildNotificationChannel(CHANNEL_NEW_CHAPTERS, IMPORTANCE_DEFAULT) {
|
||||||
|
setName(context.getString(R.string.channel_new_chapters))
|
||||||
|
},
|
||||||
|
buildNotificationChannel(CHANNEL_DOWNLOADER_PROGRESS, IMPORTANCE_LOW) {
|
||||||
|
setName(context.getString(R.string.channel_progress))
|
||||||
|
setGroup(GROUP_DOWNLOADER)
|
||||||
|
setShowBadge(false)
|
||||||
|
},
|
||||||
|
buildNotificationChannel(CHANNEL_DOWNLOADER_COMPLETE, IMPORTANCE_LOW) {
|
||||||
|
setName(context.getString(R.string.channel_complete))
|
||||||
|
setGroup(GROUP_DOWNLOADER)
|
||||||
|
setShowBadge(false)
|
||||||
|
},
|
||||||
|
buildNotificationChannel(CHANNEL_DOWNLOADER_ERROR, IMPORTANCE_LOW) {
|
||||||
|
setName(context.getString(R.string.channel_errors))
|
||||||
|
setGroup(GROUP_DOWNLOADER)
|
||||||
|
setShowBadge(false)
|
||||||
|
},
|
||||||
|
buildNotificationChannel(CHANNEL_BACKUP_RESTORE_PROGRESS, IMPORTANCE_LOW) {
|
||||||
|
setName(context.getString(R.string.channel_progress))
|
||||||
|
setGroup(GROUP_BACKUP_RESTORE)
|
||||||
|
setShowBadge(false)
|
||||||
|
},
|
||||||
|
buildNotificationChannel(CHANNEL_BACKUP_RESTORE_COMPLETE, IMPORTANCE_HIGH) {
|
||||||
|
setName(context.getString(R.string.channel_complete))
|
||||||
|
setGroup(GROUP_BACKUP_RESTORE)
|
||||||
|
setShowBadge(false)
|
||||||
|
setSound(null, null)
|
||||||
|
},
|
||||||
|
buildNotificationChannel(CHANNEL_CRASH_LOGS, IMPORTANCE_HIGH) {
|
||||||
|
setName(context.getString(R.string.channel_crash_logs))
|
||||||
|
},
|
||||||
|
buildNotificationChannel(CHANNEL_INCOGNITO_MODE, IMPORTANCE_LOW) {
|
||||||
|
setName(context.getString(R.string.pref_incognito_mode))
|
||||||
|
},
|
||||||
|
buildNotificationChannel(CHANNEL_UPDATES_TO_EXTS, IMPORTANCE_DEFAULT) {
|
||||||
|
setName(context.getString(R.string.channel_ext_updates))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
// 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"
|
||||||
@ -147,11 +151,12 @@ object PreferenceKeys {
|
|||||||
const val librarySortingMode = "library_sorting_mode"
|
const val librarySortingMode = "library_sorting_mode"
|
||||||
const val librarySortingDirection = "library_sorting_ascending"
|
const val librarySortingDirection = "library_sorting_ascending"
|
||||||
|
|
||||||
|
const val migrationSortingMode = "pref_migration_sorting"
|
||||||
|
const val migrationSortingDirection = "pref_migration_direction"
|
||||||
|
|
||||||
const val automaticExtUpdates = "automatic_ext_updates"
|
const val automaticExtUpdates = "automatic_ext_updates"
|
||||||
|
|
||||||
const val showNsfwSource = "show_nsfw_source"
|
const val showNsfwSource = "show_nsfw_source"
|
||||||
const val showNsfwExtension = "show_nsfw_extension"
|
|
||||||
const val labelNsfwExtension = "label_nsfw_extension"
|
|
||||||
|
|
||||||
const val startScreen = "start_screen"
|
const val startScreen = "start_screen"
|
||||||
|
|
||||||
@ -169,17 +174,15 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val autoUpdateTrackers = "auto_update_trackers"
|
const val autoUpdateTrackers = "auto_update_trackers"
|
||||||
|
|
||||||
const val showLibraryUpdateErrors = "show_library_update_errors"
|
|
||||||
|
|
||||||
const val downloadNew = "download_new"
|
const val downloadNew = "download_new"
|
||||||
|
|
||||||
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 +223,10 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val incognitoMode = "incognito_mode"
|
const val incognitoMode = "incognito_mode"
|
||||||
|
|
||||||
|
const val tabletUiMode = "tablet_ui_mode"
|
||||||
|
|
||||||
|
const val extensionInstaller = "extension_installer"
|
||||||
|
|
||||||
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,23 @@ 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,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ExtensionInstaller {
|
||||||
|
LEGACY,
|
||||||
|
PACKAGEINSTALLER,
|
||||||
|
SHIZUKU
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,14 +9,17 @@ 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.browse.migration.sources.MigrationSourcesController
|
||||||
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
|
||||||
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.MiuiUtil
|
||||||
|
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
|
||||||
@ -85,8 +88,6 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun autoUpdateTrackers() = prefs.getBoolean(Keys.autoUpdateTrackers, false)
|
fun autoUpdateTrackers() = prefs.getBoolean(Keys.autoUpdateTrackers, false)
|
||||||
|
|
||||||
fun showLibraryUpdateErrors() = prefs.getBoolean(Keys.showLibraryUpdateErrors, true)
|
|
||||||
|
|
||||||
fun themeMode() = flowPrefs.getEnum(Keys.themeMode, system)
|
fun themeMode() = flowPrefs.getEnum(Keys.themeMode, system)
|
||||||
|
|
||||||
fun appTheme() = flowPrefs.getEnum(Keys.appTheme, Values.AppTheme.DEFAULT)
|
fun appTheme() = flowPrefs.getEnum(Keys.appTheme, Values.AppTheme.DEFAULT)
|
||||||
@ -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)
|
||||||
@ -185,7 +190,7 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun sourceDisplayMode() = flowPrefs.getEnum(Keys.sourceDisplayMode, DisplayModeSetting.COMPACT_GRID)
|
fun sourceDisplayMode() = flowPrefs.getEnum(Keys.sourceDisplayMode, DisplayModeSetting.COMPACT_GRID)
|
||||||
|
|
||||||
fun enabledLanguages() = flowPrefs.getStringSet(Keys.enabledLanguages, setOf("en", Locale.getDefault().language))
|
fun enabledLanguages() = flowPrefs.getStringSet(Keys.enabledLanguages, setOf("all", "en", Locale.getDefault().language))
|
||||||
|
|
||||||
fun trackUsername(sync: TrackService) = prefs.getString(Keys.trackUsername(sync.id), "")
|
fun trackUsername(sync: TrackService) = prefs.getString(Keys.trackUsername(sync.id), "")
|
||||||
|
|
||||||
@ -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))
|
||||||
@ -259,14 +268,16 @@ class PreferencesHelper(val context: Context) {
|
|||||||
fun librarySortingMode() = flowPrefs.getEnum(Keys.librarySortingMode, SortModeSetting.ALPHABETICAL)
|
fun librarySortingMode() = flowPrefs.getEnum(Keys.librarySortingMode, SortModeSetting.ALPHABETICAL)
|
||||||
fun librarySortingAscending() = flowPrefs.getEnum(Keys.librarySortingDirection, SortDirectionSetting.ASCENDING)
|
fun librarySortingAscending() = flowPrefs.getEnum(Keys.librarySortingDirection, SortDirectionSetting.ASCENDING)
|
||||||
|
|
||||||
|
fun migrationSortingMode() = flowPrefs.getEnum(Keys.migrationSortingMode, MigrationSourcesController.SortSetting.ALPHABETICAL)
|
||||||
|
fun migrationSortingDirection() = flowPrefs.getEnum(Keys.migrationSortingDirection, MigrationSourcesController.DirectionSetting.ASCENDING)
|
||||||
|
|
||||||
fun automaticExtUpdates() = flowPrefs.getBoolean(Keys.automaticExtUpdates, true)
|
fun automaticExtUpdates() = flowPrefs.getBoolean(Keys.automaticExtUpdates, true)
|
||||||
|
|
||||||
fun showNsfwSource() = flowPrefs.getBoolean(Keys.showNsfwSource, true)
|
fun showNsfwSource() = flowPrefs.getBoolean(Keys.showNsfwSource, true)
|
||||||
fun showNsfwExtension() = flowPrefs.getBoolean(Keys.showNsfwExtension, true)
|
|
||||||
fun labelNsfwExtension() = prefs.getBoolean(Keys.labelNsfwExtension, true)
|
|
||||||
|
|
||||||
fun extensionUpdatesCount() = flowPrefs.getInt("ext_updates_count", 0)
|
fun extensionUpdatesCount() = flowPrefs.getInt("ext_updates_count", 0)
|
||||||
|
|
||||||
|
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 +291,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 +321,16 @@ 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 extensionInstaller() = flowPrefs.getEnum(
|
||||||
|
Keys.extensionInstaller,
|
||||||
|
if (MiuiUtil.isMiui()) Values.ExtensionInstaller.LEGACY else Values.ExtensionInstaller.PACKAGEINSTALLER
|
||||||
|
)
|
||||||
|
|
||||||
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,20 +38,22 @@ 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"
|
||||||
tracking_url = url
|
tracking_url = url
|
||||||
total_chapters = progress.booksCount
|
total_chapters = progress.maxNumberSort.toInt()
|
||||||
status = when (progress.booksCount) {
|
status = when (progress.booksCount) {
|
||||||
progress.booksUnreadCount -> Komga.UNREAD
|
progress.booksUnreadCount -> Komga.UNREAD
|
||||||
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,23 @@ 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(),
|
||||||
|
booksCount.toFloat(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ReadProgressV2Dto(
|
||||||
|
val booksCount: Int,
|
||||||
|
val booksReadCount: Int,
|
||||||
|
val booksUnreadCount: Int,
|
||||||
|
val booksInProgressCount: Int,
|
||||||
|
val lastReadContinuousNumberSort: Float,
|
||||||
|
val maxNumberSort: 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
|
||||||
|
@ -227,14 +227,26 @@ class ExtensionManager(
|
|||||||
return installExtension(availableExt)
|
return installExtension(availableExt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun cancelInstallUpdateExtension(extension: Extension) {
|
||||||
|
installer.cancelInstall(extension.pkgName)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the result of the installation of an extension.
|
* Sets to "installing" status of an extension installation.
|
||||||
*
|
*
|
||||||
* @param downloadId The id of the download.
|
* @param downloadId The id of the download.
|
||||||
* @param result Whether the extension was installed or not.
|
|
||||||
*/
|
*/
|
||||||
|
fun setInstalling(downloadId: Long) {
|
||||||
|
installer.updateInstallStep(downloadId, InstallStep.Installing)
|
||||||
|
}
|
||||||
|
|
||||||
fun setInstallationResult(downloadId: Long, result: Boolean) {
|
fun setInstallationResult(downloadId: Long, result: Boolean) {
|
||||||
installer.setInstallationResult(downloadId, result)
|
val step = if (result) InstallStep.Installed else InstallStep.Error
|
||||||
|
installer.updateInstallStep(downloadId, step)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateInstallStep(downloadId: Long, step: InstallStep) {
|
||||||
|
installer.updateInstallStep(downloadId, step)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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)
|
||||||
|
@ -10,10 +10,7 @@ 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 kotlinx.serialization.json.JsonArray
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.int
|
|
||||||
import kotlinx.serialization.json.jsonObject
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
@ -27,8 +24,8 @@ internal class ExtensionGithubApi {
|
|||||||
networkService.client
|
networkService.client
|
||||||
.newCall(GET("${REPO_URL_PREFIX}index.min.json"))
|
.newCall(GET("${REPO_URL_PREFIX}index.min.json"))
|
||||||
.await()
|
.await()
|
||||||
.parseAs<JsonArray>()
|
.parseAs<List<ExtensionJsonObject>>()
|
||||||
.let { parseResponse(it) }
|
.toExtensions()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,24 +52,23 @@ internal class ExtensionGithubApi {
|
|||||||
return extensionsWithUpdate
|
return extensionsWithUpdate
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseResponse(json: JsonArray): List<Extension.Available> {
|
private fun List<ExtensionJsonObject>.toExtensions(): List<Extension.Available> {
|
||||||
return json
|
return this
|
||||||
.filter { element ->
|
.filter {
|
||||||
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
|
val libVersion = it.version.substringBeforeLast('.').toDouble()
|
||||||
val libVersion = versionName.substringBeforeLast('.').toDouble()
|
|
||||||
libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX
|
libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX
|
||||||
}
|
}
|
||||||
.map { element ->
|
.map {
|
||||||
val name = element.jsonObject["name"]!!.jsonPrimitive.content.substringAfter("Tachiyomi: ")
|
Extension.Available(
|
||||||
val pkgName = element.jsonObject["pkg"]!!.jsonPrimitive.content
|
name = it.name.substringAfter("Tachiyomi: "),
|
||||||
val apkName = element.jsonObject["apk"]!!.jsonPrimitive.content
|
pkgName = it.pkg,
|
||||||
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
|
versionName = it.version,
|
||||||
val versionCode = element.jsonObject["code"]!!.jsonPrimitive.int
|
versionCode = it.code,
|
||||||
val lang = element.jsonObject["lang"]!!.jsonPrimitive.content
|
lang = it.lang,
|
||||||
val nsfw = element.jsonObject["nsfw"]!!.jsonPrimitive.int == 1
|
isNsfw = it.nsfw == 1,
|
||||||
val icon = "${REPO_URL_PREFIX}icon/${apkName.replace(".apk", ".png")}"
|
apkName = it.apk,
|
||||||
|
iconUrl = "${REPO_URL_PREFIX}icon/${it.apk.replace(".apk", ".png")}"
|
||||||
Extension.Available(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,3 +78,14 @@ internal class ExtensionGithubApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private const val REPO_URL_PREFIX = "https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/"
|
private const val REPO_URL_PREFIX = "https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/"
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class ExtensionJsonObject(
|
||||||
|
val name: String,
|
||||||
|
val pkg: String,
|
||||||
|
val apk: String,
|
||||||
|
val version: String,
|
||||||
|
val code: Long,
|
||||||
|
val lang: String,
|
||||||
|
val nsfw: Int,
|
||||||
|
)
|
||||||
|
@ -0,0 +1,170 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.installer
|
||||||
|
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.annotation.CallSuper
|
||||||
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
|
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.util.Collections
|
||||||
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base implementation class for extension installer. To be used inside a foreground [Service].
|
||||||
|
*/
|
||||||
|
abstract class Installer(private val service: Service) {
|
||||||
|
|
||||||
|
private val extensionManager: ExtensionManager by injectLazy()
|
||||||
|
|
||||||
|
private var waitingInstall = AtomicReference<Entry>(null)
|
||||||
|
private val queue = Collections.synchronizedList(mutableListOf<Entry>())
|
||||||
|
|
||||||
|
private val cancelReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
val downloadId = intent.getLongExtra(EXTRA_DOWNLOAD_ID, -1).takeIf { it >= 0 } ?: return
|
||||||
|
cancelQueue(downloadId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Installer readiness. If false, queue check will not run.
|
||||||
|
*
|
||||||
|
* @see checkQueue
|
||||||
|
*/
|
||||||
|
abstract var ready: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an item to install queue.
|
||||||
|
*
|
||||||
|
* @param downloadId Download ID as known by [ExtensionManager]
|
||||||
|
* @param uri Uri of APK to install
|
||||||
|
*/
|
||||||
|
fun addToQueue(downloadId: Long, uri: Uri) {
|
||||||
|
queue.add(Entry(downloadId, uri))
|
||||||
|
checkQueue()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proceeds to install the APK of this entry inside this method. Call [continueQueue]
|
||||||
|
* when the install process for this entry is finished to continue the queue.
|
||||||
|
*
|
||||||
|
* @param entry The [Entry] of item to process
|
||||||
|
* @see continueQueue
|
||||||
|
*/
|
||||||
|
@CallSuper
|
||||||
|
open fun processEntry(entry: Entry) {
|
||||||
|
extensionManager.setInstalling(entry.downloadId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called before queue continues. Override this to handle when the removed entry is
|
||||||
|
* currently being processed.
|
||||||
|
*
|
||||||
|
* @return true if this entry can be removed from queue.
|
||||||
|
*/
|
||||||
|
open fun cancelEntry(entry: Entry): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells the queue to continue processing the next entry and updates the install step
|
||||||
|
* of the completed entry ([waitingInstall]) to [ExtensionManager].
|
||||||
|
*
|
||||||
|
* @param resultStep new install step for the processed entry.
|
||||||
|
* @see waitingInstall
|
||||||
|
*/
|
||||||
|
fun continueQueue(resultStep: InstallStep) {
|
||||||
|
val completedEntry = waitingInstall.getAndSet(null)
|
||||||
|
if (completedEntry != null) {
|
||||||
|
extensionManager.updateInstallStep(completedEntry.downloadId, resultStep)
|
||||||
|
checkQueue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the queue. The provided service will be stopped if the queue is empty.
|
||||||
|
* Will not be run when not ready.
|
||||||
|
*
|
||||||
|
* @see ready
|
||||||
|
*/
|
||||||
|
fun checkQueue() {
|
||||||
|
if (!ready) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (queue.isEmpty()) {
|
||||||
|
service.stopSelf()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val nextEntry = queue.first()
|
||||||
|
if (waitingInstall.compareAndSet(null, nextEntry)) {
|
||||||
|
queue.removeFirst()
|
||||||
|
processEntry(nextEntry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call this method when the provided service is destroyed.
|
||||||
|
*/
|
||||||
|
@CallSuper
|
||||||
|
open fun onDestroy() {
|
||||||
|
LocalBroadcastManager.getInstance(service).unregisterReceiver(cancelReceiver)
|
||||||
|
queue.forEach { extensionManager.updateInstallStep(it.downloadId, InstallStep.Error) }
|
||||||
|
queue.clear()
|
||||||
|
waitingInstall.set(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun getActiveEntry(): Entry? = waitingInstall.get()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels queue for the provided download ID if exists.
|
||||||
|
*
|
||||||
|
* @param downloadId Download ID as known by [ExtensionManager]
|
||||||
|
*/
|
||||||
|
private fun cancelQueue(downloadId: Long) {
|
||||||
|
val waitingInstall = this.waitingInstall.get()
|
||||||
|
val toCancel = queue.find { it.downloadId == downloadId } ?: waitingInstall ?: return
|
||||||
|
if (cancelEntry(toCancel)) {
|
||||||
|
queue.remove(toCancel)
|
||||||
|
if (waitingInstall == toCancel) {
|
||||||
|
// Currently processing removed entry, continue queue
|
||||||
|
this.waitingInstall.set(null)
|
||||||
|
checkQueue()
|
||||||
|
}
|
||||||
|
extensionManager.updateInstallStep(downloadId, InstallStep.Idle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install item to queue.
|
||||||
|
*
|
||||||
|
* @param downloadId Download ID as known by [ExtensionManager]
|
||||||
|
* @param uri Uri of APK to install
|
||||||
|
*/
|
||||||
|
data class Entry(val downloadId: Long, val uri: Uri)
|
||||||
|
|
||||||
|
init {
|
||||||
|
val filter = IntentFilter(ACTION_CANCEL_QUEUE)
|
||||||
|
LocalBroadcastManager.getInstance(service).registerReceiver(cancelReceiver, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ACTION_CANCEL_QUEUE = "Installer.action.CANCEL_QUEUE"
|
||||||
|
private const val EXTRA_DOWNLOAD_ID = "Installer.extra.DOWNLOAD_ID"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to cancel the installation entry for the provided download ID.
|
||||||
|
*
|
||||||
|
* @param downloadId Download ID as known by [ExtensionManager]
|
||||||
|
*/
|
||||||
|
fun cancelInstallQueue(context: Context, downloadId: Long) {
|
||||||
|
val intent = Intent(ACTION_CANCEL_QUEUE)
|
||||||
|
intent.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
|
||||||
|
LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
105
app/src/main/java/eu/kanade/tachiyomi/extension/installer/PackageInstallerInstaller.kt
Normal file
105
app/src/main/java/eu/kanade/tachiyomi/extension/installer/PackageInstallerInstaller.kt
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.installer
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.content.pm.PackageInstaller
|
||||||
|
import android.os.Build
|
||||||
|
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||||
|
import eu.kanade.tachiyomi.util.lang.use
|
||||||
|
import eu.kanade.tachiyomi.util.system.getUriSize
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
class PackageInstallerInstaller(private val service: Service) : Installer(service) {
|
||||||
|
|
||||||
|
private val packageInstaller = service.packageManager.packageInstaller
|
||||||
|
|
||||||
|
private val packageActionReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
when (intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE)) {
|
||||||
|
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||||
|
val userAction = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
|
||||||
|
if (userAction == null) {
|
||||||
|
Timber.e("Fatal error for $intent")
|
||||||
|
continueQueue(InstallStep.Error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userAction.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
service.startActivity(userAction)
|
||||||
|
}
|
||||||
|
PackageInstaller.STATUS_FAILURE_ABORTED -> {
|
||||||
|
continueQueue(InstallStep.Idle)
|
||||||
|
}
|
||||||
|
PackageInstaller.STATUS_SUCCESS -> continueQueue(InstallStep.Installed)
|
||||||
|
else -> continueQueue(InstallStep.Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var activeSession: Pair<Entry, Int>? = null
|
||||||
|
|
||||||
|
// Always ready
|
||||||
|
override var ready = true
|
||||||
|
|
||||||
|
override fun processEntry(entry: Entry) {
|
||||||
|
super.processEntry(entry)
|
||||||
|
activeSession = null
|
||||||
|
try {
|
||||||
|
val installParams = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
installParams.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED)
|
||||||
|
}
|
||||||
|
activeSession = entry to packageInstaller.createSession(installParams)
|
||||||
|
val fileSize = service.getUriSize(entry.uri) ?: throw IllegalStateException()
|
||||||
|
installParams.setSize(fileSize)
|
||||||
|
|
||||||
|
val inputStream = service.contentResolver.openInputStream(entry.uri) ?: throw IllegalStateException()
|
||||||
|
val session = packageInstaller.openSession(activeSession!!.second)
|
||||||
|
val outputStream = session.openWrite(entry.downloadId.toString(), 0, fileSize)
|
||||||
|
session.use {
|
||||||
|
arrayOf(inputStream, outputStream).use {
|
||||||
|
inputStream.copyTo(outputStream)
|
||||||
|
session.fsync(outputStream)
|
||||||
|
}
|
||||||
|
|
||||||
|
val intentSender = PendingIntent.getBroadcast(
|
||||||
|
service,
|
||||||
|
activeSession!!.second,
|
||||||
|
Intent(INSTALL_ACTION),
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0
|
||||||
|
).intentSender
|
||||||
|
session.commit(intentSender)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "Failed to install extension ${entry.downloadId} ${entry.uri}")
|
||||||
|
activeSession?.let { (_, sessionId) ->
|
||||||
|
packageInstaller.abandonSession(sessionId)
|
||||||
|
}
|
||||||
|
continueQueue(InstallStep.Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun cancelEntry(entry: Entry): Boolean {
|
||||||
|
activeSession?.let { (activeEntry, sessionId) ->
|
||||||
|
if (activeEntry == entry) {
|
||||||
|
packageInstaller.abandonSession(sessionId)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
service.unregisterReceiver(packageActionReceiver)
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
service.registerReceiver(packageActionReceiver, IntentFilter(INSTALL_ACTION))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val INSTALL_ACTION = "PackageInstallerInstaller.INSTALL_ACTION"
|
@ -0,0 +1,127 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.installer
|
||||||
|
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||||
|
import eu.kanade.tachiyomi.util.system.getUriSize
|
||||||
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import rikka.shizuku.Shizuku
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.BufferedReader
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
class ShizukuInstaller(private val service: Service) : Installer(service) {
|
||||||
|
|
||||||
|
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
|
private val shizukuDeadListener = Shizuku.OnBinderDeadListener {
|
||||||
|
Timber.e("Shizuku was killed prematurely")
|
||||||
|
service.stopSelf()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val shizukuPermissionListener = object : Shizuku.OnRequestPermissionResultListener {
|
||||||
|
override fun onRequestPermissionResult(requestCode: Int, grantResult: Int) {
|
||||||
|
if (requestCode == SHIZUKU_PERMISSION_REQUEST_CODE) {
|
||||||
|
if (grantResult == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
ready = true
|
||||||
|
checkQueue()
|
||||||
|
} else {
|
||||||
|
service.stopSelf()
|
||||||
|
}
|
||||||
|
Shizuku.removeRequestPermissionResultListener(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override var ready = false
|
||||||
|
|
||||||
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
override fun processEntry(entry: Entry) {
|
||||||
|
super.processEntry(entry)
|
||||||
|
ioScope.launch {
|
||||||
|
var sessionId: String? = null
|
||||||
|
try {
|
||||||
|
val size = service.getUriSize(entry.uri) ?: throw IllegalStateException()
|
||||||
|
service.contentResolver.openInputStream(entry.uri)!!.use {
|
||||||
|
val createCommand = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
"pm install-create --user current -i ${service.packageName} -S $size"
|
||||||
|
} else {
|
||||||
|
"pm install-create -i ${service.packageName} -S $size"
|
||||||
|
}
|
||||||
|
val createResult = exec(createCommand)
|
||||||
|
sessionId = SESSION_ID_REGEX.find(createResult.out)?.value
|
||||||
|
?: throw RuntimeException("Failed to create install session")
|
||||||
|
|
||||||
|
val writeResult = exec("pm install-write -S $size $sessionId base -", it)
|
||||||
|
if (writeResult.resultCode != 0) {
|
||||||
|
throw RuntimeException("Failed to write APK to session $sessionId")
|
||||||
|
}
|
||||||
|
|
||||||
|
val commitResult = exec("pm install-commit $sessionId")
|
||||||
|
if (commitResult.resultCode != 0) {
|
||||||
|
throw RuntimeException("Failed to commit install session $sessionId")
|
||||||
|
}
|
||||||
|
|
||||||
|
continueQueue(InstallStep.Installed)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "Failed to install extension ${entry.downloadId} ${entry.uri}")
|
||||||
|
if (sessionId != null) {
|
||||||
|
exec("pm install-abandon $sessionId")
|
||||||
|
}
|
||||||
|
continueQueue(InstallStep.Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't cancel if entry is already started installing
|
||||||
|
override fun cancelEntry(entry: Entry): Boolean = getActiveEntry() != entry
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
Shizuku.removeBinderDeadListener(shizukuDeadListener)
|
||||||
|
Shizuku.removeRequestPermissionResultListener(shizukuPermissionListener)
|
||||||
|
ioScope.cancel()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun exec(command: String, stdin: InputStream? = null): ShellResult {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val process = Shizuku.newProcess(arrayOf("sh", "-c", command), null, null)
|
||||||
|
if (stdin != null) {
|
||||||
|
process.outputStream.use { stdin.copyTo(it) }
|
||||||
|
}
|
||||||
|
val output = process.inputStream.bufferedReader().use(BufferedReader::readText)
|
||||||
|
val resultCode = process.waitFor()
|
||||||
|
return ShellResult(resultCode, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class ShellResult(val resultCode: Int, val out: String)
|
||||||
|
|
||||||
|
init {
|
||||||
|
Shizuku.addBinderDeadListener(shizukuDeadListener)
|
||||||
|
ready = if (Shizuku.pingBinder()) {
|
||||||
|
if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
Shizuku.addRequestPermissionResultListener(shizukuPermissionListener)
|
||||||
|
Shizuku.requestPermission(SHIZUKU_PERMISSION_REQUEST_CODE)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Timber.e("Shizuku is not ready to use.")
|
||||||
|
service.toast(R.string.ext_installer_shizuku_stopped)
|
||||||
|
service.stopSelf()
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val SHIZUKU_PERMISSION_REQUEST_CODE = 14045
|
||||||
|
private val SESSION_ID_REGEX = Regex("(?<=\\[).+?(?=])")
|
@ -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
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
package eu.kanade.tachiyomi.extension.model
|
package eu.kanade.tachiyomi.extension.model
|
||||||
|
|
||||||
enum class InstallStep {
|
enum class InstallStep {
|
||||||
Pending, Downloading, Installing, Installed, Error;
|
Idle, Pending, Downloading, Installing, Installed, Error;
|
||||||
|
|
||||||
fun isCompleted(): Boolean {
|
fun isCompleted(): Boolean {
|
||||||
return this == Installed || this == Error
|
return this == Installed || this == Error || this == Idle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import android.app.Activity
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
|
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
@ -40,13 +41,14 @@ class ExtensionInstallActivity : Activity() {
|
|||||||
|
|
||||||
private fun checkInstallationResult(resultCode: Int) {
|
private fun checkInstallationResult(resultCode: Int) {
|
||||||
val downloadId = intent.extras!!.getLong(ExtensionInstaller.EXTRA_DOWNLOAD_ID)
|
val downloadId = intent.extras!!.getLong(ExtensionInstaller.EXTRA_DOWNLOAD_ID)
|
||||||
val success = resultCode == RESULT_OK
|
|
||||||
|
|
||||||
val extensionManager = Injekt.get<ExtensionManager>()
|
val extensionManager = Injekt.get<ExtensionManager>()
|
||||||
extensionManager.setInstallationResult(downloadId, success)
|
val newStep = when (resultCode) {
|
||||||
}
|
RESULT_OK -> InstallStep.Installed
|
||||||
|
RESULT_CANCELED -> InstallStep.Idle
|
||||||
private companion object {
|
else -> InstallStep.Error
|
||||||
const val INSTALL_REQUEST_CODE = 500
|
}
|
||||||
|
extensionManager.updateInstallStep(downloadId, newStep)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val INSTALL_REQUEST_CODE = 500
|
||||||
|
@ -0,0 +1,82 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.util
|
||||||
|
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.IBinder
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||||
|
import eu.kanade.tachiyomi.extension.installer.Installer
|
||||||
|
import eu.kanade.tachiyomi.extension.installer.PackageInstallerInstaller
|
||||||
|
import eu.kanade.tachiyomi.extension.installer.ShizukuInstaller
|
||||||
|
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID
|
||||||
|
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
class ExtensionInstallService : Service() {
|
||||||
|
|
||||||
|
private var installer: Installer? = null
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
val notification = notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
|
||||||
|
setSmallIcon(R.drawable.ic_tachi)
|
||||||
|
setAutoCancel(false)
|
||||||
|
setOngoing(true)
|
||||||
|
setShowWhen(false)
|
||||||
|
setContentTitle(getString(R.string.ext_install_service_notif))
|
||||||
|
setProgress(100, 100, true)
|
||||||
|
}.build()
|
||||||
|
startForeground(Notifications.ID_EXTENSION_INSTALLER, notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
val uri = intent?.data
|
||||||
|
val id = intent?.getLongExtra(EXTRA_DOWNLOAD_ID, -1)?.takeIf { it != -1L }
|
||||||
|
val installerUsed = intent?.getSerializableExtra(EXTRA_INSTALLER) as? PreferenceValues.ExtensionInstaller
|
||||||
|
if (uri == null || id == null || installerUsed == null) {
|
||||||
|
stopSelf()
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
if (installer == null) {
|
||||||
|
installer = when (installerUsed) {
|
||||||
|
PreferenceValues.ExtensionInstaller.PACKAGEINSTALLER -> PackageInstallerInstaller(this)
|
||||||
|
PreferenceValues.ExtensionInstaller.SHIZUKU -> ShizukuInstaller(this)
|
||||||
|
else -> {
|
||||||
|
Timber.e("Not implemented for installer $installerUsed")
|
||||||
|
stopSelf()
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
installer!!.addToQueue(id, uri)
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
installer?.onDestroy()
|
||||||
|
installer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(i: Intent?): IBinder? = null
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val EXTRA_INSTALLER = "EXTRA_INSTALLER"
|
||||||
|
|
||||||
|
fun getIntent(
|
||||||
|
context: Context,
|
||||||
|
downloadId: Long,
|
||||||
|
uri: Uri,
|
||||||
|
installer: PreferenceValues.ExtensionInstaller
|
||||||
|
): Intent {
|
||||||
|
return Intent(context, ExtensionInstallService::class.java)
|
||||||
|
.setDataAndType(uri, ExtensionInstaller.APK_MIME)
|
||||||
|
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
|
||||||
|
.putExtra(EXTRA_INSTALLER, installer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -7,15 +7,21 @@ import android.content.Intent
|
|||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.jakewharton.rxrelay.PublishRelay
|
import com.jakewharton.rxrelay.PublishRelay
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.extension.installer.Installer
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
@ -47,6 +53,8 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
private val downloadsRelay = PublishRelay.create<Pair<Long, InstallStep>>()
|
private val downloadsRelay = PublishRelay.create<Pair<Long, InstallStep>>()
|
||||||
|
|
||||||
|
private val installerPref = Injekt.get<PreferencesHelper>().extensionInstaller()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds the given extension to the downloads queue and returns an observable containing its
|
* Adds the given extension to the downloads queue and returns an observable containing its
|
||||||
* step in the installation process.
|
* step in the installation process.
|
||||||
@ -79,8 +87,6 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||||||
.map { it.second }
|
.map { it.second }
|
||||||
// Poll download status
|
// Poll download status
|
||||||
.mergeWith(pollStatus(id))
|
.mergeWith(pollStatus(id))
|
||||||
// Force an error if the download takes more than 3 minutes
|
|
||||||
.mergeWith(Observable.timer(3, TimeUnit.MINUTES).map { InstallStep.Error })
|
|
||||||
// Stop when the application is installed or errors
|
// Stop when the application is installed or errors
|
||||||
.takeUntil { it.isCompleted() }
|
.takeUntil { it.isCompleted() }
|
||||||
// Always notify on main thread
|
// Always notify on main thread
|
||||||
@ -126,12 +132,29 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||||||
* @param uri The uri of the extension to install.
|
* @param uri The uri of the extension to install.
|
||||||
*/
|
*/
|
||||||
fun installApk(downloadId: Long, uri: Uri) {
|
fun installApk(downloadId: Long, uri: Uri) {
|
||||||
val intent = Intent(context, ExtensionInstallActivity::class.java)
|
when (val installer = installerPref.get()) {
|
||||||
.setDataAndType(uri, APK_MIME)
|
PreferenceValues.ExtensionInstaller.LEGACY -> {
|
||||||
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
|
val intent = Intent(context, ExtensionInstallActivity::class.java)
|
||||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
.setDataAndType(uri, APK_MIME)
|
||||||
|
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
|
||||||
|
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
val intent = ExtensionInstallService.getIntent(context, downloadId, uri, installer)
|
||||||
|
ContextCompat.startForegroundService(context, intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels extension install and remove from download manager and installer.
|
||||||
|
*/
|
||||||
|
fun cancelInstall(pkgName: String) {
|
||||||
|
val downloadId = activeDownloads.remove(pkgName) ?: return
|
||||||
|
downloadManager.remove(downloadId)
|
||||||
|
Installer.cancelInstallQueue(context, downloadId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -147,13 +170,12 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the result of the installation of an extension.
|
* Sets the step of the installation of an extension.
|
||||||
*
|
*
|
||||||
* @param downloadId The id of the download.
|
* @param downloadId The id of the download.
|
||||||
* @param result Whether the extension was installed or not.
|
* @param step New install step.
|
||||||
*/
|
*/
|
||||||
fun setInstallationResult(downloadId: Long, result: Boolean) {
|
fun updateInstallStep(downloadId: Long, step: InstallStep) {
|
||||||
val step = if (result) InstallStep.Installed else InstallStep.Error
|
|
||||||
downloadsRelay.call(downloadId to step)
|
downloadsRelay.call(downloadId to step)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,9 +238,7 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||||||
val uri = downloadManager.getUriForDownloadedFile(id)
|
val uri = downloadManager.getUriForDownloadedFile(id)
|
||||||
|
|
||||||
// Set next installation step
|
// Set next installation step
|
||||||
if (uri != null) {
|
if (uri == null) {
|
||||||
downloadsRelay.call(id to InstallStep.Installing)
|
|
||||||
} else {
|
|
||||||
Timber.e("Couldn't locate downloaded APK")
|
Timber.e("Couldn't locate downloaded APK")
|
||||||
downloadsRelay.call(id to InstallStep.Error)
|
downloadsRelay.call(id to InstallStep.Error)
|
||||||
return
|
return
|
||||||
|
@ -4,8 +4,8 @@ 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.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
import eu.kanade.tachiyomi.extension.model.LoadResult
|
||||||
@ -103,7 +103,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")
|
||||||
@ -153,13 +153,7 @@ internal object ExtensionLoader {
|
|||||||
try {
|
try {
|
||||||
when (val obj = Class.forName(it, false, classLoader).newInstance()) {
|
when (val obj = Class.forName(it, false, classLoader).newInstance()) {
|
||||||
is Source -> listOf(obj)
|
is Source -> listOf(obj)
|
||||||
is SourceFactory -> {
|
is SourceFactory -> obj.createSources()
|
||||||
if (isSourceNsfw(obj)) {
|
|
||||||
emptyList()
|
|
||||||
} else {
|
|
||||||
obj.createSources()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> throw Exception("Unknown source class type! ${obj.javaClass}")
|
else -> throw Exception("Unknown source class type! ${obj.javaClass}")
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@ -167,7 +161,6 @@ internal object ExtensionLoader {
|
|||||||
return LoadResult.Error(e)
|
return LoadResult.Error(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.filter { !isSourceNsfw(it) }
|
|
||||||
|
|
||||||
val langs = sources.filterIsInstance<CatalogueSource>()
|
val langs = sources.filterIsInstance<CatalogueSource>()
|
||||||
.map { it.lang }
|
.map { it.lang }
|
||||||
@ -214,22 +207,4 @@ internal object ExtensionLoader {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether a Source or SourceFactory is annotated with @Nsfw.
|
|
||||||
*/
|
|
||||||
private fun isSourceNsfw(clazz: Any): Boolean {
|
|
||||||
if (loadNsfwSource) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clazz !is Source && clazz !is SourceFactory) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Annotations are proxied, hence this janky way of checking for them
|
|
||||||
return clazz.javaClass.annotations
|
|
||||||
.flatMap { it.javaClass.interfaces.map { it.simpleName } }
|
|
||||||
.firstOrNull { it == Nsfw::class.java.simpleName } != null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
@ -263,18 +271,13 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
throw Exception(context.getString(R.string.chapter_not_found))
|
throw Exception(context.getString(R.string.chapter_not_found))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFormat(file: File): Format {
|
private fun getFormat(file: File) = with(file) {
|
||||||
val extension = file.extension
|
when {
|
||||||
return if (file.isDirectory) {
|
isDirectory -> Format.Directory(this)
|
||||||
Format.Directory(file)
|
extension.equals("zip", true) || extension.equals("cbz", true) -> Format.Zip(this)
|
||||||
} else if (extension.equals("zip", true) || extension.equals("cbz", true)) {
|
extension.equals("rar", true) || extension.equals("cbr", true) -> Format.Rar(this)
|
||||||
Format.Zip(file)
|
extension.equals("epub", true) -> Format.Epub(this)
|
||||||
} else if (extension.equals("rar", true) || extension.equals("cbr", true)) {
|
else -> throw Exception(context.getString(R.string.local_invalid_format))
|
||||||
Format.Rar(file)
|
|
||||||
} else if (extension.equals("epub", true)) {
|
|
||||||
Format.Epub(file)
|
|
||||||
} else {
|
|
||||||
throw Exception(context.getString(R.string.local_invalid_format))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +11,6 @@ import rx.Observable
|
|||||||
open class SourceManager(private val context: Context) {
|
open class SourceManager(private val context: Context) {
|
||||||
|
|
||||||
private val sourcesMap = mutableMapOf<Long, Source>()
|
private val sourcesMap = mutableMapOf<Long, Source>()
|
||||||
|
|
||||||
private val stubSourcesMap = mutableMapOf<Long, StubSource>()
|
private val stubSourcesMap = mutableMapOf<Long, StubSource>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@ -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) {
|
||||||
|
@ -22,5 +22,6 @@ class ExtensionAdapter(controller: ExtensionController) :
|
|||||||
|
|
||||||
interface OnButtonClickListener {
|
interface OnButtonClickListener {
|
||||||
fun onButtonClick(position: Int)
|
fun onButtonClick(position: Int)
|
||||||
|
fun onCancelButtonClick(position: Int)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,6 +119,11 @@ open class ExtensionController :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCancelButtonClick(position: Int) {
|
||||||
|
val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return
|
||||||
|
presenter.cancelInstallUpdateExtension(extension)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
inflater.inflate(R.menu.browse_extensions, menu)
|
inflater.inflate(R.menu.browse_extensions, menu)
|
||||||
|
|
||||||
|
@ -1,31 +1,28 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.extension
|
package eu.kanade.tachiyomi.ui.browse.extension
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import coil.clear
|
import coil.clear
|
||||||
import coil.load
|
import coil.load
|
||||||
import eu.davidea.viewholders.FlexibleViewHolder
|
import eu.davidea.viewholders.FlexibleViewHolder
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import eu.kanade.tachiyomi.databinding.ExtensionCardItemBinding
|
import eu.kanade.tachiyomi.databinding.ExtensionCardItemBinding
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
|
class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
|
||||||
FlexibleViewHolder(view, adapter) {
|
FlexibleViewHolder(view, adapter) {
|
||||||
|
|
||||||
private val binding = ExtensionCardItemBinding.bind(view)
|
private val binding = ExtensionCardItemBinding.bind(view)
|
||||||
|
|
||||||
private val shouldLabelNsfw by lazy {
|
|
||||||
Injekt.get<PreferencesHelper>().labelNsfwExtension()
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
binding.extButton.setOnClickListener {
|
binding.extButton.setOnClickListener {
|
||||||
adapter.buttonClickListener.onButtonClick(bindingAdapterPosition)
|
adapter.buttonClickListener.onButtonClick(bindingAdapterPosition)
|
||||||
}
|
}
|
||||||
|
binding.cancelButton.setOnClickListener {
|
||||||
|
adapter.buttonClickListener.onCancelButtonClick(bindingAdapterPosition)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bind(item: ExtensionItem) {
|
fun bind(item: ExtensionItem) {
|
||||||
@ -38,7 +35,7 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
|
|||||||
extension is Extension.Untrusted -> itemView.context.getString(R.string.ext_untrusted)
|
extension is Extension.Untrusted -> itemView.context.getString(R.string.ext_untrusted)
|
||||||
extension is Extension.Installed && extension.isUnofficial -> itemView.context.getString(R.string.ext_unofficial)
|
extension is Extension.Installed && extension.isUnofficial -> itemView.context.getString(R.string.ext_unofficial)
|
||||||
extension is Extension.Installed && extension.isObsolete -> itemView.context.getString(R.string.ext_obsolete)
|
extension is Extension.Installed && extension.isObsolete -> itemView.context.getString(R.string.ext_obsolete)
|
||||||
extension.isNsfw && shouldLabelNsfw -> itemView.context.getString(R.string.ext_nsfw_short)
|
extension.isNsfw -> itemView.context.getString(R.string.ext_nsfw_short)
|
||||||
else -> ""
|
else -> ""
|
||||||
}.uppercase()
|
}.uppercase()
|
||||||
|
|
||||||
@ -48,44 +45,40 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
|
|||||||
} else {
|
} else {
|
||||||
extension.getApplicationIcon(itemView.context)?.let { binding.image.setImageDrawable(it) }
|
extension.getApplicationIcon(itemView.context)?.let { binding.image.setImageDrawable(it) }
|
||||||
}
|
}
|
||||||
bindButton(item)
|
bindButtons(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("ResourceType")
|
@Suppress("ResourceType")
|
||||||
fun bindButton(item: ExtensionItem) = with(binding.extButton) {
|
fun bindButtons(item: ExtensionItem) = with(binding.extButton) {
|
||||||
isEnabled = true
|
|
||||||
isClickable = true
|
|
||||||
|
|
||||||
val extension = item.extension
|
val extension = item.extension
|
||||||
|
|
||||||
val installStep = item.installStep
|
val installStep = item.installStep
|
||||||
if (installStep != null) {
|
setText(
|
||||||
setText(
|
when (installStep) {
|
||||||
when (installStep) {
|
InstallStep.Pending -> R.string.ext_pending
|
||||||
InstallStep.Pending -> R.string.ext_pending
|
InstallStep.Downloading -> R.string.ext_downloading
|
||||||
InstallStep.Downloading -> R.string.ext_downloading
|
InstallStep.Installing -> R.string.ext_installing
|
||||||
InstallStep.Installing -> R.string.ext_installing
|
InstallStep.Installed -> R.string.ext_installed
|
||||||
InstallStep.Installed -> R.string.ext_installed
|
InstallStep.Error -> R.string.action_retry
|
||||||
InstallStep.Error -> R.string.action_retry
|
InstallStep.Idle -> {
|
||||||
}
|
when (extension) {
|
||||||
)
|
is Extension.Installed -> {
|
||||||
if (installStep != InstallStep.Error) {
|
if (extension.hasUpdate) {
|
||||||
isEnabled = false
|
R.string.ext_update
|
||||||
isClickable = false
|
} else {
|
||||||
}
|
R.string.action_settings
|
||||||
} else if (extension is Extension.Installed) {
|
}
|
||||||
when {
|
}
|
||||||
extension.hasUpdate -> {
|
is Extension.Untrusted -> R.string.ext_trust
|
||||||
setText(R.string.ext_update)
|
is Extension.Available -> R.string.ext_install
|
||||||
}
|
}
|
||||||
else -> {
|
|
||||||
setText(R.string.action_settings)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (extension is Extension.Untrusted) {
|
)
|
||||||
setText(R.string.ext_trust)
|
|
||||||
} else {
|
val isIdle = installStep == InstallStep.Idle || installStep == InstallStep.Error
|
||||||
setText(R.string.ext_install)
|
binding.cancelButton.isVisible = !isIdle
|
||||||
}
|
isEnabled = isIdle
|
||||||
|
isClickable = isIdle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ import eu.kanade.tachiyomi.source.CatalogueSource
|
|||||||
data class ExtensionItem(
|
data class ExtensionItem(
|
||||||
val extension: Extension,
|
val extension: Extension,
|
||||||
val header: ExtensionGroupItem? = null,
|
val header: ExtensionGroupItem? = null,
|
||||||
val installStep: InstallStep? = null
|
val installStep: InstallStep = InstallStep.Idle
|
||||||
) :
|
) :
|
||||||
AbstractSectionableItem<ExtensionHolder, ExtensionGroupItem>(header) {
|
AbstractSectionableItem<ExtensionHolder, ExtensionGroupItem>(header) {
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ data class ExtensionItem(
|
|||||||
if (payloads == null || payloads.isEmpty()) {
|
if (payloads == null || payloads.isEmpty()) {
|
||||||
holder.bind(this)
|
holder.bind(this)
|
||||||
} else {
|
} else {
|
||||||
holder.bindButton(this)
|
holder.bindButtons(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,14 +55,14 @@ open class ExtensionPresenter(
|
|||||||
private fun toItems(tuple: ExtensionTuple): List<ExtensionItem> {
|
private fun toItems(tuple: ExtensionTuple): List<ExtensionItem> {
|
||||||
val context = Injekt.get<Application>()
|
val context = Injekt.get<Application>()
|
||||||
val activeLangs = preferences.enabledLanguages().get()
|
val activeLangs = preferences.enabledLanguages().get()
|
||||||
val showNsfwExtensions = preferences.showNsfwExtension().get()
|
val showNsfwSources = preferences.showNsfwSource().get()
|
||||||
|
|
||||||
val (installed, untrusted, available) = tuple
|
val (installed, untrusted, available) = tuple
|
||||||
|
|
||||||
val items = mutableListOf<ExtensionItem>()
|
val items = mutableListOf<ExtensionItem>()
|
||||||
|
|
||||||
val updatesSorted = installed.filter { it.hasUpdate && (showNsfwExtensions || !it.isNsfw) }.sortedBy { it.name }
|
val updatesSorted = installed.filter { it.hasUpdate && (showNsfwSources || !it.isNsfw) }.sortedBy { it.name }
|
||||||
val installedSorted = installed.filter { !it.hasUpdate && (showNsfwExtensions || !it.isNsfw) }.sortedWith(compareBy({ !it.isObsolete }, { it.name }))
|
val installedSorted = installed.filter { !it.hasUpdate && (showNsfwSources || !it.isNsfw) }.sortedWith(compareBy({ !it.isObsolete }, { it.name }))
|
||||||
val untrustedSorted = untrusted.sortedBy { it.name }
|
val untrustedSorted = untrusted.sortedBy { it.name }
|
||||||
val availableSorted = available
|
val availableSorted = available
|
||||||
// Filter out already installed extensions and disabled languages
|
// Filter out already installed extensions and disabled languages
|
||||||
@ -70,21 +70,21 @@ open class ExtensionPresenter(
|
|||||||
installed.none { it.pkgName == avail.pkgName } &&
|
installed.none { it.pkgName == avail.pkgName } &&
|
||||||
untrusted.none { it.pkgName == avail.pkgName } &&
|
untrusted.none { it.pkgName == avail.pkgName } &&
|
||||||
(avail.lang in activeLangs || avail.lang == "all") &&
|
(avail.lang in activeLangs || avail.lang == "all") &&
|
||||||
(showNsfwExtensions || !avail.isNsfw)
|
(showNsfwSources || !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)
|
||||||
items += updatesSorted.map { extension ->
|
items += updatesSorted.map { extension ->
|
||||||
ExtensionItem(extension, header, currentDownloads[extension.pkgName])
|
ExtensionItem(extension, header, currentDownloads[extension.pkgName] ?: InstallStep.Idle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (installedSorted.isNotEmpty() || untrustedSorted.isNotEmpty()) {
|
if (installedSorted.isNotEmpty() || untrustedSorted.isNotEmpty()) {
|
||||||
val header = ExtensionGroupItem(context.getString(R.string.ext_installed), installedSorted.size + untrustedSorted.size)
|
val header = ExtensionGroupItem(context.getString(R.string.ext_installed), installedSorted.size + untrustedSorted.size)
|
||||||
|
|
||||||
items += installedSorted.map { extension ->
|
items += installedSorted.map { extension ->
|
||||||
ExtensionItem(extension, header, currentDownloads[extension.pkgName])
|
ExtensionItem(extension, header, currentDownloads[extension.pkgName] ?: InstallStep.Idle)
|
||||||
}
|
}
|
||||||
|
|
||||||
items += untrustedSorted.map { extension ->
|
items += untrustedSorted.map { extension ->
|
||||||
@ -100,7 +100,7 @@ open class ExtensionPresenter(
|
|||||||
.forEach {
|
.forEach {
|
||||||
val header = ExtensionGroupItem(it.key, it.value.size)
|
val header = ExtensionGroupItem(it.key, it.value.size)
|
||||||
items += it.value.map { extension ->
|
items += it.value.map { extension ->
|
||||||
ExtensionItem(extension, header, currentDownloads[extension.pkgName])
|
ExtensionItem(extension, header, currentDownloads[extension.pkgName] ?: InstallStep.Idle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -133,6 +133,10 @@ open class ExtensionPresenter(
|
|||||||
extensionManager.updateExtension(extension).subscribeToInstallUpdate(extension)
|
extensionManager.updateExtension(extension).subscribeToInstallUpdate(extension)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun cancelInstallUpdateExtension(extension: Extension) {
|
||||||
|
extensionManager.cancelInstallUpdateExtension(extension)
|
||||||
|
}
|
||||||
|
|
||||||
private fun Observable<InstallStep>.subscribeToInstallUpdate(extension: Extension) {
|
private fun Observable<InstallStep>.subscribeToInstallUpdate(extension: Extension) {
|
||||||
this.doOnNext { currentDownloads[extension.pkgName] = it }
|
this.doOnNext { currentDownloads[extension.pkgName] = it }
|
||||||
.doOnUnsubscribe { currentDownloads.remove(extension.pkgName) }
|
.doOnUnsubscribe { currentDownloads.remove(extension.pkgName) }
|
||||||
|
@ -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"
|
||||||
|
@ -2,10 +2,7 @@ package eu.kanade.tachiyomi.ui.browse.extension.details
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.Settings
|
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
@ -14,7 +11,6 @@ import android.view.MenuItem
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.appcompat.view.ContextThemeWrapper
|
import androidx.appcompat.view.ContextThemeWrapper
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.preference.Preference
|
|
||||||
import androidx.preference.PreferenceGroupAdapter
|
import androidx.preference.PreferenceGroupAdapter
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
@ -34,7 +30,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 +44,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()
|
||||||
|
|
||||||
@ -70,7 +64,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun createPresenter(): ExtensionDetailsPresenter {
|
override fun createPresenter(): ExtensionDetailsPresenter {
|
||||||
return ExtensionDetailsPresenter(args.getString(PKGNAME_KEY)!!)
|
return ExtensionDetailsPresenter(this, args.getString(PKGNAME_KEY)!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getTitle(): String? {
|
override fun getTitle(): String? {
|
||||||
@ -108,72 +102,87 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
|
|||||||
val isMultiLangSingleSource = isMultiSource && extension.sources.map { it.name }.distinct().size == 1
|
val isMultiLangSingleSource = isMultiSource && extension.sources.map { it.name }.distinct().size == 1
|
||||||
|
|
||||||
with(screen) {
|
with(screen) {
|
||||||
extension.sources
|
if (isMultiSource && isMultiLangSingleSource.not()) {
|
||||||
.groupBy { (it as CatalogueSource).lang }
|
multiLanguagePreference(context, extension.sources)
|
||||||
.toSortedMap(compareBy { LocaleHelper.getSourceDisplayName(it, context) })
|
} else {
|
||||||
.forEach {
|
singleLanguagePreference(context, extension.sources)
|
||||||
val preferenceBlock = {
|
}
|
||||||
it.value
|
|
||||||
.sortedWith(compareBy({ !it.isEnabled() }, { it.name.lowercase() }))
|
|
||||||
.forEach { source ->
|
|
||||||
val sourcePrefs = mutableListOf<Preference>()
|
|
||||||
|
|
||||||
val block: (@DSL SwitchPreferenceCompat).() -> Unit = {
|
|
||||||
key = source.getPreferenceKey()
|
|
||||||
title = when {
|
|
||||||
isMultiSource && !isMultiLangSingleSource -> source.toString()
|
|
||||||
else -> LocaleHelper.getSourceDisplayName(it.key, context)
|
|
||||||
}
|
|
||||||
isPersistent = false
|
|
||||||
isChecked = source.isEnabled()
|
|
||||||
|
|
||||||
onChange { newValue ->
|
|
||||||
val checked = newValue as Boolean
|
|
||||||
toggleSource(source, checked)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
// React to enable/disable all changes
|
|
||||||
preferences.disabledSources().asFlow()
|
|
||||||
.onEach {
|
|
||||||
val enabled = source.isEnabled()
|
|
||||||
isChecked = enabled
|
|
||||||
sourcePrefs.forEach { pref -> pref.isVisible = enabled }
|
|
||||||
}
|
|
||||||
.launchIn(viewScope)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Source enable/disable
|
|
||||||
if (source is ConfigurableSource) {
|
|
||||||
switchSettingsPreference {
|
|
||||||
block()
|
|
||||||
onSettingsClick = View.OnClickListener {
|
|
||||||
router.pushController(
|
|
||||||
SourcePreferencesController(source.id).withFadeTransaction()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switchPreference(block)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMultiSource && !isMultiLangSingleSource) {
|
|
||||||
preferenceCategory {
|
|
||||||
title = LocaleHelper.getSourceDisplayName(it.key, context)
|
|
||||||
|
|
||||||
preferenceBlock()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
preferenceBlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return PreferenceGroupAdapter(screen)
|
return PreferenceGroupAdapter(screen)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun PreferenceScreen.singleLanguagePreference(context: Context, sources: List<Source>) {
|
||||||
|
sources
|
||||||
|
.map { source -> LocaleHelper.getSourceDisplayName(source.lang, context) to source }
|
||||||
|
.sortedWith(compareBy({ (_, source) -> !source.isEnabled() }, { (lang, _) -> lang.lowercase() }))
|
||||||
|
.forEach { (lang, source) ->
|
||||||
|
val preferenceBlock = {
|
||||||
|
sourceSwitchPreference(source, LocaleHelper.getSourceDisplayName(lang, context))
|
||||||
|
}
|
||||||
|
|
||||||
|
preferenceBlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun PreferenceScreen.multiLanguagePreference(context: Context, sources: List<Source>) {
|
||||||
|
sources
|
||||||
|
.groupBy { (it as CatalogueSource).lang }
|
||||||
|
.toSortedMap(compareBy { LocaleHelper.getSourceDisplayName(it, context) })
|
||||||
|
.forEach { entry ->
|
||||||
|
val preferenceBlock = {
|
||||||
|
entry.value
|
||||||
|
.sortedWith(compareBy({ source -> !source.isEnabled() }, { source -> source.name.lowercase() }))
|
||||||
|
.forEach { source ->
|
||||||
|
sourceSwitchPreference(source, source.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
preferenceCategory {
|
||||||
|
title = LocaleHelper.getSourceDisplayName(entry.key, context)
|
||||||
|
|
||||||
|
preferenceBlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun PreferenceScreen.sourceSwitchPreference(source: Source, name: String) {
|
||||||
|
val block: (@DSL SwitchPreferenceCompat).() -> Unit = {
|
||||||
|
key = source.getPreferenceKey()
|
||||||
|
title = name
|
||||||
|
isPersistent = false
|
||||||
|
isChecked = source.isEnabled()
|
||||||
|
|
||||||
|
onChange { newValue ->
|
||||||
|
val checked = newValue as Boolean
|
||||||
|
toggleSource(source, checked)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
// React to enable/disable all changes
|
||||||
|
preferences.disabledSources().asFlow()
|
||||||
|
.onEach {
|
||||||
|
val enabled = source.isEnabled()
|
||||||
|
isChecked = enabled
|
||||||
|
}
|
||||||
|
.launchIn(viewScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Source enable/disable
|
||||||
|
if (source is ConfigurableSource) {
|
||||||
|
switchSettingsPreference {
|
||||||
|
block()
|
||||||
|
onSettingsClick = View.OnClickListener {
|
||||||
|
router.pushController(
|
||||||
|
SourcePreferencesController(source.id).withFadeTransaction()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switchPreference(block)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroyView(view: View) {
|
override fun onDestroyView(view: View) {
|
||||||
preferenceScreen = null
|
preferenceScreen = null
|
||||||
super.onDestroyView(view)
|
super.onDestroyView(view)
|
||||||
@ -190,7 +199,6 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
|
|||||||
R.id.action_history -> openCommitHistory()
|
R.id.action_history -> openCommitHistory()
|
||||||
R.id.action_enable_all -> toggleAllSources(true)
|
R.id.action_enable_all -> toggleAllSources(true)
|
||||||
R.id.action_disable_all -> toggleAllSources(false)
|
R.id.action_disable_all -> toggleAllSources(false)
|
||||||
R.id.action_open_in_settings -> openInSettings()
|
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
@ -221,13 +229,6 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
|
|||||||
openInBrowser(url)
|
openInBrowser(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openInSettings() {
|
|
||||||
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
|
||||||
data = Uri.fromParts("package", presenter.pkgName, null)
|
|
||||||
}
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Source.isEnabled(): Boolean {
|
private fun Source.isEnabled(): Boolean {
|
||||||
return id.toString() !in preferences.disabledSources().get()
|
return id.toString() !in preferences.disabledSources().get()
|
||||||
}
|
}
|
||||||
@ -237,10 +238,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"
|
||||||
|
25
app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsHeaderAdapter.kt
25
app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsHeaderAdapter.kt
@ -34,25 +34,28 @@ 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)
|
||||||
|
binding.btnAppInfo.clicks()
|
||||||
|
.onEach { presenter.openInSettings() }
|
||||||
|
.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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,21 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.extension.details
|
package eu.kanade.tachiyomi.ui.browse.extension.details
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.provider.Settings
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.injectLazy
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
class ExtensionDetailsPresenter(
|
class ExtensionDetailsPresenter(
|
||||||
val pkgName: String,
|
private val controller: ExtensionDetailsController,
|
||||||
private val extensionManager: ExtensionManager = Injekt.get()
|
private val pkgName: String,
|
||||||
) : BasePresenter<ExtensionDetailsController>() {
|
) : BasePresenter<ExtensionDetailsController>() {
|
||||||
|
|
||||||
|
private val extensionManager: ExtensionManager by injectLazy()
|
||||||
|
|
||||||
val extension = extensionManager.installedExtensions.find { it.pkgName == pkgName }
|
val extension = extensionManager.installedExtensions.find { it.pkgName == pkgName }
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
@ -36,4 +40,11 @@ class ExtensionDetailsPresenter(
|
|||||||
val extension = extension ?: return
|
val extension = extension ?: return
|
||||||
extensionManager.uninstallExtension(extension.pkgName)
|
extensionManager.uninstallExtension(extension.pkgName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun openInSettings() {
|
||||||
|
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||||
|
data = Uri.fromParts("package", pkgName, null)
|
||||||
|
}
|
||||||
|
controller.startActivity(intent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user